feat: migrate CRM screens to Ant Design Vue and expand home dashboard
Preserve the local Ant UI work across views/components and add the new home dashboard chart panels before merging latest main.
This commit is contained in:
@@ -2,13 +2,25 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import { RobotOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
Button as AButton,
|
||||
Col as ACol,
|
||||
Form as AForm,
|
||||
Input as AInput,
|
||||
Modal as AModal,
|
||||
Row as ARow,
|
||||
Select as ASelect,
|
||||
Space as ASpace,
|
||||
Tag as ATag,
|
||||
Typography,
|
||||
} from 'ant-design-vue';
|
||||
import VoiceToTextButton from '@/components/VoiceToTextButton.vue';
|
||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||
import { formatLocalDateTimeInput, rewriteGregorianDates } from '@/lib/jalali';
|
||||
import { UiButton, UiModal } from '@/components/ui';
|
||||
import { api } from '@/lib/api';
|
||||
import { brand, type ActivityType, type AskMentoSourceDto, type PersonDto, type VoiceCommandDraft } from '@mento/shared';
|
||||
import { brand, type AskMentoSourceDto, type PersonDto, type VoiceCommandDraft } from '@mento/shared';
|
||||
|
||||
const props = defineProps<{
|
||||
askQuery: string;
|
||||
@@ -26,11 +38,23 @@ const emit = defineEmits<{
|
||||
notice: [value: { error: string; done: string }];
|
||||
}>();
|
||||
|
||||
const { Text: ATypographyText, Paragraph: ATypographyParagraph } = Typography;
|
||||
|
||||
const AFormItem = AForm.Item;
|
||||
const ATextarea = AInput.TextArea;
|
||||
|
||||
const shortcuts = [
|
||||
{ label: 'معاملات باز و در خطر را بگو', icon: 'warning' as const },
|
||||
{ label: 'فعالیتهای عقبافتاده چیست؟', icon: 'calendar' as const },
|
||||
{ label: 'مهمترین پیگیری امروز چیست؟', icon: 'send' as const },
|
||||
{ label: 'خلاصه وضعیت فروش را بده', icon: 'chart' as const },
|
||||
{ label: 'معاملات در خطر', prompt: 'معاملات باز و در خطر را بگو' },
|
||||
{ label: 'عقبافتادهها', prompt: 'فعالیتهای عقبافتاده چیست؟' },
|
||||
{ label: 'پیگیری امروز', prompt: 'مهمترین پیگیری امروز چیست؟' },
|
||||
{ label: 'خلاصه فروش', prompt: 'خلاصه وضعیت فروش را بده' },
|
||||
];
|
||||
|
||||
const activityTypeOptions = [
|
||||
{ value: 'call', label: 'تماس' },
|
||||
{ value: 'meeting', label: 'جلسه' },
|
||||
{ value: 'task', label: 'کار' },
|
||||
{ value: 'note', label: 'یادداشت' },
|
||||
];
|
||||
|
||||
function sourceTo(source: AskMentoSourceDto) {
|
||||
@@ -45,13 +69,6 @@ function pick(label: string) {
|
||||
emit('ask', label);
|
||||
}
|
||||
|
||||
const ACTIVITY_LABEL: Record<ActivityType, string> = {
|
||||
call: 'تماس',
|
||||
meeting: 'جلسه',
|
||||
task: 'کار',
|
||||
note: 'یادداشت',
|
||||
};
|
||||
|
||||
const voiceBuf = ref('');
|
||||
const voiceBusy = ref(false);
|
||||
const voiceError = ref('');
|
||||
@@ -60,6 +77,13 @@ const draft = ref<VoiceCommandDraft | null>(null);
|
||||
const confirming = ref(false);
|
||||
const answerOpen = ref(false);
|
||||
|
||||
const draftOpen = computed({
|
||||
get: () => Boolean(draft.value && draft.value.intent !== 'none'),
|
||||
set: (open: boolean) => {
|
||||
if (!open) cancelDraft();
|
||||
},
|
||||
});
|
||||
|
||||
function apiMessage(e: unknown, fallback: string) {
|
||||
if (!axios.isAxiosError(e)) return fallback;
|
||||
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
||||
@@ -272,12 +296,36 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article id="home-ai-assistant" class="panel">
|
||||
<header class="panel-head">
|
||||
<h2>
|
||||
<span class="spark"><UiIcon name="sparkle" :size="15" /></span>
|
||||
{{ brand.aiNameFa }}
|
||||
</h2>
|
||||
<section id="home-ai-assistant" class="bg-white rounded-2xl border border-slate-200/80 p-3 shadow-sm flex flex-col justify-between h-[330px] gap-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<span class="w-10 h-10 rounded-xl bg-emerald-600 text-white flex items-center justify-center shrink-0">
|
||||
<RobotOutlined />
|
||||
</span>
|
||||
<h3 class="font-bold text-slate-800 text-base truncate">{{ brand.aiNameFa }}</h3>
|
||||
</div>
|
||||
<a-tag color="success" class="!m-0 shrink-0">هوش مصنوعی</a-tag>
|
||||
</div>
|
||||
|
||||
<div class="bg-emerald-50/50 border border-emerald-200/60 rounded-xl p-3 text-xs text-slate-600 leading-relaxed">
|
||||
<template v-if="answer">{{ answer }}</template>
|
||||
<template v-else-if="error">{{ error }}</template>
|
||||
<template v-else>
|
||||
<strong class="text-slate-800">پیشنهاد هوشمند امروز:</strong>
|
||||
از پیشنهادهای سریع استفاده کنید یا پرسش خود را بنویسید.
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-50 border border-slate-200/90 rounded-xl p-1.5 flex items-center gap-2">
|
||||
<a-input
|
||||
:value="askQuery"
|
||||
:placeholder="brand.askPlaceholderFa"
|
||||
:bordered="false"
|
||||
:disabled="asking || voiceBusy || confirming"
|
||||
class="flex-1"
|
||||
@update:value="emit('update:askQuery', $event)"
|
||||
@press-enter="onTypedSubmit"
|
||||
/>
|
||||
<VoiceToTextButton
|
||||
variant="toolbar"
|
||||
hide-error
|
||||
@@ -286,322 +334,181 @@ watch(
|
||||
@recording="onVoiceRecording"
|
||||
@error="voiceError = $event"
|
||||
/>
|
||||
</header>
|
||||
|
||||
<p class="ai-lead">چه کمکی میتونم بکنم؟</p>
|
||||
|
||||
<div class="ai-shortcuts">
|
||||
<button
|
||||
v-for="item in shortcuts"
|
||||
:key="item.label"
|
||||
class="ai-chip"
|
||||
type="button"
|
||||
:disabled="asking"
|
||||
@click="pick(item.label)"
|
||||
>
|
||||
<span class="chip-ico"><UiIcon :name="item.icon" :size="14" /></span>
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="voiceBusy" class="ai-answer"><p>در حال فهمیدن دستور…</p></div>
|
||||
<div v-else-if="confirming" class="ai-answer"><p>در حال ثبت…</p></div>
|
||||
<div v-else-if="voiceDone" class="ai-answer"><p>{{ voiceDone }}</p></div>
|
||||
<p v-else-if="voiceError" class="ai-error">{{ voiceError }}</p>
|
||||
<p v-else-if="error" class="ai-error">{{ error }}</p>
|
||||
<div v-else-if="asking" class="ai-answer">
|
||||
<p>در حال پاسخ…</p>
|
||||
</div>
|
||||
|
||||
<form class="ai-input" @submit.prevent="onTypedSubmit">
|
||||
<input
|
||||
:value="askQuery"
|
||||
type="search"
|
||||
:placeholder="brand.askPlaceholderFa"
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="asking || voiceBusy || confirming"
|
||||
@input="emit('update:askQuery', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button type="submit" aria-label="ارسال" :disabled="asking || voiceBusy || confirming">
|
||||
<UiIcon name="send" :size="15" />
|
||||
</button>
|
||||
</form>
|
||||
:loading="asking || voiceBusy"
|
||||
aria-label="ارسال"
|
||||
@click="onTypedSubmit"
|
||||
>
|
||||
بپرس
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<UiModal
|
||||
:open="Boolean(draft && draft.intent !== 'none')"
|
||||
<div class="pt-3 border-t border-slate-100 flex items-center justify-between flex-wrap gap-2">
|
||||
<span class="text-[11px] font-semibold text-slate-400">پیشنهادهای سریع</span>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
v-for="item in shortcuts"
|
||||
:key="item.label"
|
||||
type="button"
|
||||
class="!text-[11px] !leading-4 !font-medium bg-emerald-50/70 text-emerald-700 border border-emerald-200/60 px-2 py-0.5 rounded-md disabled:opacity-50"
|
||||
:disabled="asking"
|
||||
@click="pick(item.prompt)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-alert v-if="voiceBusy" type="info" show-icon message="در حال فهمیدن دستور…" />
|
||||
<a-alert v-else-if="confirming" type="info" show-icon message="در حال ثبت…" />
|
||||
<a-alert v-else-if="voiceDone" type="success" show-icon :message="voiceDone" />
|
||||
<a-alert v-else-if="voiceError" type="error" show-icon :message="voiceError" />
|
||||
</section>
|
||||
|
||||
<a-modal
|
||||
v-model:open="draftOpen"
|
||||
:title="draft?.intent === 'create_person' ? 'تأیید ساخت مشتری' : 'تأیید ثبت فعالیت'"
|
||||
@close="cancelDraft"
|
||||
destroy-on-close
|
||||
@cancel="cancelDraft"
|
||||
>
|
||||
<p v-if="draft" class="modal-lead">{{ rewriteGregorianDates(draft.summary) }}</p>
|
||||
<p class="edit-hint">قبل از تأیید میتوانید متن را اصلاح کنید.</p>
|
||||
<div v-if="draft?.person" class="modal-fields">
|
||||
<label>نام <input v-model="draft.person.firstName" @input="onPersonNameEdit" /></label>
|
||||
<label>نام خانوادگی <input v-model="draft.person.lastName" /></label>
|
||||
<label>موبایل <input v-model="draft.person.mobile" dir="ltr" /></label>
|
||||
<label>تلفن <input v-model="draft.person.phone" dir="ltr" /></label>
|
||||
<label>ایمیل <input v-model="draft.person.email" dir="ltr" /></label>
|
||||
<label>وبسایت <input v-model="draft.person.website" dir="ltr" /></label>
|
||||
<label>معرف <input v-model="draft.person.referrer" /></label>
|
||||
<label>سمت <input v-model="draft.person.position" /></label>
|
||||
<label>یادداشت <textarea v-model="draft.person.notes" rows="2" /></label>
|
||||
</div>
|
||||
<div v-else-if="draft?.activity" class="modal-fields">
|
||||
<label>عنوان <input v-model="draft.activity.title" @input="onActivityTitleEdit" /></label>
|
||||
<label>
|
||||
نوع
|
||||
<select v-model="draft.activity.type">
|
||||
<option v-for="(label, key) in ACTIVITY_LABEL" :key="key" :value="key">{{ label }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
زمان
|
||||
<JalaliDateInput :model-value="dueLocal" mode="datetime" @update:model-value="onDueLocal" />
|
||||
</label>
|
||||
<label>
|
||||
مشتری
|
||||
<input v-model="draft.activity.personName" @input="onActivityPersonEdit" @change="rematchActivityPerson" />
|
||||
</label>
|
||||
<p v-if="draft.activity.personName && !draft.activity.personId" class="warn">
|
||||
در فهرست مشتریان پروندهای با این نام پیدا نشد. فعالیت ذخیره میشود، ولی داخل پروندهٔ آن شخص دیده نمیشود.
|
||||
</p>
|
||||
<label>توضیح <textarea v-model="draft.activity.description" rows="2" /></label>
|
||||
</div>
|
||||
<p v-if="draft?.missing.length" class="ai-error">ناقص است: {{ draft.missing.join('، ') }}</p>
|
||||
<template #footer>
|
||||
<UiButton variant="ghost" :disabled="confirming" @click="cancelDraft">انصراف</UiButton>
|
||||
<UiButton variant="primary" :disabled="!draft?.canConfirm || confirming" :loading="confirming" @click="confirmDraft">
|
||||
تأیید و انجام
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
<a-typography-paragraph v-if="draft">
|
||||
{{ rewriteGregorianDates(draft.summary) }}
|
||||
</a-typography-paragraph>
|
||||
<a-typography-text type="secondary">قبل از تأیید میتوانید متن را اصلاح کنید.</a-typography-text>
|
||||
|
||||
<UiModal :open="answerOpen && Boolean(answer)" title="پاسخ دستیار" @close="closeAnswer">
|
||||
<p class="modal-lead answer-body">{{ rewriteGregorianDates(answer || '') }}</p>
|
||||
<div v-if="sources?.length" class="sources">
|
||||
<a-form v-if="draft?.person" layout="vertical" style="margin-top: 16px">
|
||||
<a-row :gutter="[16, 0]">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="نام">
|
||||
<a-input v-model:value="draft.person.firstName" @update:value="onPersonNameEdit" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="نام خانوادگی">
|
||||
<a-input v-model:value="draft.person.lastName" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="موبایل">
|
||||
<a-input v-model:value="draft.person.mobile" dir="ltr" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="تلفن">
|
||||
<a-input v-model:value="draft.person.phone" dir="ltr" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="ایمیل">
|
||||
<a-input v-model:value="draft.person.email" dir="ltr" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="وبسایت">
|
||||
<a-input v-model:value="draft.person.website" dir="ltr" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="معرف">
|
||||
<a-input v-model:value="draft.person.referrer" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="سمت">
|
||||
<a-input v-model:value="draft.person.position" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="یادداشت">
|
||||
<a-textarea v-model:value="draft.person.notes" :rows="2" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
|
||||
<a-form v-else-if="draft?.activity" layout="vertical" style="margin-top: 16px">
|
||||
<a-row :gutter="[16, 0]">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="عنوان">
|
||||
<a-input v-model:value="draft.activity.title" @update:value="onActivityTitleEdit" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="نوع">
|
||||
<a-select
|
||||
v-model:value="draft.activity.type"
|
||||
:options="activityTypeOptions"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="زمان">
|
||||
<JalaliDateInput :model-value="dueLocal" mode="datetime" @update:model-value="onDueLocal" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="مشتری">
|
||||
<a-input
|
||||
v-model:value="draft.activity.personName"
|
||||
@update:value="onActivityPersonEdit"
|
||||
@blur="rematchActivityPerson"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-alert
|
||||
v-if="draft.activity.personName && !draft.activity.personId"
|
||||
type="warning"
|
||||
show-icon
|
||||
message="در فهرست مشتریان پروندهای با این نام پیدا نشد. فعالیت ذخیره میشود، ولی داخل پروندهٔ آن شخص دیده نمیشود."
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="توضیح">
|
||||
<a-textarea v-model:value="draft.activity.description" :rows="2" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
|
||||
<a-alert
|
||||
v-if="draft?.missing.length"
|
||||
type="error"
|
||||
show-icon
|
||||
:message="`ناقص است: ${draft.missing.join('، ')}`"
|
||||
style="margin-top: 12px"
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<a-button :disabled="confirming" @click="cancelDraft">انصراف</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!draft?.canConfirm || confirming"
|
||||
:loading="confirming"
|
||||
@click="confirmDraft"
|
||||
>
|
||||
تأیید و انجام
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:open="answerOpen" title="پاسخ دستیار" destroy-on-close @cancel="closeAnswer">
|
||||
<a-typography-paragraph style="white-space: pre-wrap">
|
||||
{{ rewriteGregorianDates(answer || '') }}
|
||||
</a-typography-paragraph>
|
||||
<a-space v-if="sources?.length" wrap>
|
||||
<RouterLink
|
||||
v-for="source in sources"
|
||||
:key="`${source.type}-${source.id}`"
|
||||
:to="sourceTo(source)"
|
||||
@click="closeAnswer"
|
||||
>
|
||||
{{ source.label }}
|
||||
<a-tag color="processing">{{ source.label }}</a-tag>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</a-space>
|
||||
<template #footer>
|
||||
<UiButton variant="primary" @click="closeAnswer">بستن</UiButton>
|
||||
<a-button type="primary" @click="closeAnswer">بستن</a-button>
|
||||
</template>
|
||||
</UiModal>
|
||||
</article>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem 1rem 0.95rem;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid #eef2f7;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
margin-bottom: 0.55rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-lead {
|
||||
margin: 0 0 0.85rem;
|
||||
color: #334155;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.answer-body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.edit-hint {
|
||||
margin: -0.35rem 0 0.75rem;
|
||||
color: #64748b;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.modal-fields {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.modal-fields label {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.modal-fields input,
|
||||
.modal-fields select,
|
||||
.modal-fields textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
font: inherit;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 0.65rem;
|
||||
}
|
||||
|
||||
.modal-fields textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.warn {
|
||||
display: inline-block;
|
||||
margin-inline-start: 0.35rem;
|
||||
color: #b45309;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 0.92rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.spark {
|
||||
color: #eab308;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.ai-lead {
|
||||
margin: 0 0 0.75rem;
|
||||
padding: 0.55rem 0.75rem;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
background: #d1fae5;
|
||||
color: #115e59;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
border-radius: 14px 14px 4px 14px;
|
||||
}
|
||||
|
||||
.ai-shortcuts {
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.8rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ai-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
padding: 0.58rem 0.7rem;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
border: 1px solid #eef2f7;
|
||||
color: #334155;
|
||||
font-size: 0.76rem;
|
||||
text-align: start;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
|
||||
}
|
||||
|
||||
.ai-chip:hover:not(:disabled) {
|
||||
background: #f0fdfa;
|
||||
border-color: #99f6e4;
|
||||
}
|
||||
|
||||
.ai-chip:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.chip-ico {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: var(--radius);
|
||||
background: #ecfdf8;
|
||||
color: #14b8a6;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-input {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.ai-input input {
|
||||
flex: 1;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.58rem 0.75rem;
|
||||
background: #f8fafc;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.ai-input button {
|
||||
width: 40px;
|
||||
border: 0;
|
||||
border-radius: var(--radius);
|
||||
background: #14b8a6;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.ai-input button:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.ai-answer {
|
||||
background: #f0fdfa;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.7rem 0.8rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.ai-answer p {
|
||||
margin: 0;
|
||||
color: #134e4a;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.sources {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.sources a {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
color: #0f766e;
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 0.15rem 0.5rem;
|
||||
}
|
||||
|
||||
.ai-error {
|
||||
margin: 0 0 0.7rem;
|
||||
color: #b91c1c;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user