| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710 |
- <template>
- <div class="ai-chat-container">
- <div class="message-quick-box">
- <div class="message-quick-box-item" :class="{ 'active': isQuickActions.includes('quick') }">
- <button type="button" class="coco-collapse-trigger" @click="toggleQuickActions('quick')">
- <span class="coco-collapse-icon soft-orange">
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
- stroke-linecap="round">
- <path d="M4 7h16"></path>
- <path d="M4 12h10"></path>
- <path d="M4 17h7"></path>
- </svg>
- </span>
- <span class="coco-collapse-copy">
- <span class="coco-collapse-title">{{ lang.ssAiChatQuickTitle }}</span>
- <span class="coco-collapse-subtitle" id="cocoQuickSectionHint">{{ lang.ssAiChatQuickSubtitle }}</span>
- </span>
- <span class="coco-collapse-chevron">
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <polyline points="6 9 12 15 18 9"></polyline>
- </svg>
- </span>
- </button>
- <div class="coco-collapse-body" v-if="isQuickActions.includes('quick')">
- <div class="coco-quick-tabs">
- <button type="button" class="coco-quick-tab" :class="{ 'active': cocoQuickTab === 'page' }"
- @click="setCocoQuickTab('page')">{{ lang.ssAiChatQuickTabPage }}</button>
- <button type="button" class="coco-quick-tab" :class="{ 'active': cocoQuickTab === 'course' }"
- @click="setCocoQuickTab('course')">{{ lang.ssAiChatQuickTabCourse }}</button>
- </div>
- <div class="coco-quick-list">
- <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'page'"
- @click="sendQuickAction(lang.ssAiChatQuickAction3)">
- <span class="coco-quick-action-copy">
- <span class="coco-quick-action-title">{{ lang.ssAiChatQuickGenChoicesTitle }}</span>
- <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickGenChoicesDesc }}</span>
- </span>
- <span class="coco-quick-action-arrow">
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
- stroke-linecap="round" stroke-linejoin="round">
- <path d="M5 12h14"></path>
- <path d="M13 5l7 7-7 7"></path>
- </svg>
- </span>
- </button>
- <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'page'"
- @click="sendQuickAction(lang.ssAiChatQuickAction4)">
- <span class="coco-quick-action-copy">
- <span class="coco-quick-action-title">{{ lang.ssAiChatQuickGenWebTitle }}</span>
- <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickGenWebDesc }}</span>
- </span>
- <span class="coco-quick-action-arrow">
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
- stroke-linecap="round" stroke-linejoin="round">
- <path d="M5 12h14"></path>
- <path d="M13 5l7 7-7 7"></path>
- </svg>
- </span>
- </button>
- <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'course'"
- @click="sendQuickAction(lang.ssAiChatQuickAction5)">
- <span class="coco-quick-action-copy">
- <span class="coco-quick-action-title">{{ lang.ssAiChatQuickRecommendToolsTitle }}</span>
- <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickRecommendToolsDesc }}</span>
- </span>
- <span class="coco-quick-action-arrow">
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
- stroke-linecap="round" stroke-linejoin="round">
- <path d="M5 12h14"></path>
- <path d="M13 5l7 7-7 7"></path>
- </svg>
- </span>
- </button>
- <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'course'" @click="sendQuickAction(lang.ssBatchInteractiveWeb)">
- <span class="coco-quick-action-copy">
- <span class="coco-quick-action-title">{{ lang.ssAiChatQuickBatchGenToolsTitle }}</span>
- <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickBatchGenToolsDesc }}</span>
- </span>
- <span class="coco-quick-action-arrow">
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <path d="M5 12h14"></path>
- <path d="M13 5l7 7-7 7"></path>
- </svg>
- </span>
- </button>
- </div>
- </div>
- </div>
- </div>
- <!-- 聊天区域 -->
- <div class="chat-section" ref="chatSection">
- <!-- 消息列表 -->
- <div v-for="(message, index) in messages" :key="index" class="chat-message">
- <div class="message-content user-message chat" v-if="message.content">
- <div v-html="message.content"></div>
- <!-- 显示上传的文件 -->
- <div class="message-files" v-if="message.sourceFiles && message.sourceFiles.length > 0">
- <div v-for="(file, index) in message.sourceFiles" :key="index" class="message-file-item">
- <span>{{ file.title }}</span>
- </div>
- </div>
- </div>
- <div class="message-content ai-message chat" :class="{'loading-state': message.chatloading && message.jsonData?.gType === 'trigger_batch_tool'}" v-if="message.aiContent || message.chatloading">
- <div class="submenu-icon loading-icon" v-show="message.chatloading && message.jsonData?.gType === 'trigger_batch_tool'">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
- <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle>
- </svg>
- </div>
- <div v-if="message.aiContent" v-html="message.aiContent"></div>
- <svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32"
- viewBox="0 0 24 24"><!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
- <circle cx="4" cy="12" r="3" fill="currentColor">
- <animate id="svgSpinners3DotsBounce0" attributeName="cy" begin="0;svgSpinners3DotsBounce1.end+0.25s"
- calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
- </circle>
- <circle cx="12" cy="12" r="3" fill="currentColor">
- <animate attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.1s" calcMode="spline" dur="0.6s"
- keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
- </circle>
- <circle cx="20" cy="12" r="3" fill="currentColor">
- <animate id="svgSpinners3DotsBounce1" attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.2s"
- calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
- </circle>
- </svg>
- <button class="confirm-btn" :class="{ disabled: message.jsonData?.isGenerate }"
- v-if="message.jsonData?.gType !== 'chat' && message.jsonData?.gType !== 'generate_interactive_web' && message.jsonData?.gType !== 'trigger_tool_recommend' && message.jsonData?.gType !== 'trigger_batch_tool' && !message.chatloading && message.aiContent"
- @click="generate(message)">{{ message.gLoading ? lang.ssLoading : lang.ssConfirm }}</button>
- <button type="button" class="coco-interactive-web-card"
- v-if="message.jsonData?.gType === 'generate_interactive_web' && !message.chatloading" :class="{ 'loading-state': create_app_loading }" @click="!create_app_loading && handle_add_aiWeb()">
- <div class="submenu-icon loading-icon" v-show="create_app_loading">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
- <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle>
- </svg>
- </div>
- <div class="coco-interactive-web-card-head">
- <div class="coco-interactive-web-card-title">{{ lang.ssInteractiveWebTitle }}</div>
- <!-- <span class="coco-interactive-web-card-status">待确认</span> -->
- </div>
- <div class="coco-interactive-web-card-desc">{{ lang.ssInteractiveWebDesc }}</div>
- <div class="coco-interactive-web-card-foot">
- <span class="coco-interactive-web-card-action">{{ lang.ssOpenWorkspace }}</span>
- </div>
- </button>
- </div>
- </div>
- </div>
- <!-- 输入区域 -->
- <div class="input-section">
- <div class="input-wrapper">
- <div class="file-box" v-show="files.length">
- <div v-for="(file, index) in files" :key="index" class="file-item">
- <span class="file-name">{{ file.title }}</span>
- <button class="remove-file-btn" @click="removeFile(index)">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
- stroke-linejoin="round">
- <line x1="18" y1="6" x2="6" y2="18"></line>
- <line x1="6" y1="6" x2="18" y2="18"></line>
- </svg>
- </button>
- </div>
- </div>
- <textarea class="ai-input" :placeholder="lang.ssAiChatShortcut" v-model="inputText"
- @keyup.enter.exact="sendMessage" rows="5" />
- <div class="input-actions">
- <FileInput accept="*" @change="handleFileUpload">
- <button class="attach-btn">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path
- d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48">
- </path>
- </svg>
- </button>
- </FileInput>
- <button class="send-btn" @click="sendMessage" v-if="!chatLoading">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
- stroke-linejoin="round">
- <line x1="22" y1="2" x2="11" y2="13"></line>
- <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
- </svg>
- </button>
- <button class="send-btn stop" @click="stopMessage" v-if="chatLoading">
- <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
- <rect width="32" height="32" rx="16" fill="black" fill-opacity="0.4"></rect>
- <path
- d="M11.3333 12.333C11.3333 11.7807 11.781 11.333 12.3333 11.333H19.6666C20.2189 11.333 20.6666 11.7807 20.6666 12.333V19.6663C20.6666 20.2186 20.2189 20.6663 19.6666 20.6663H12.3333C11.781 20.6663 11.3333 20.2186 11.3333 19.6663V12.333Z"
- fill="white" fill-opacity="0.9"></path>
- </svg>
- </button>
- </div>
- </div>
- <!-- 输入时的快捷操作弹出 -->
- <div class="quick-actions-popup" v-if="showQuickActions">
- <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn"
- @click="sendQuickAction(action)">{{ action }}</button>
- </div>
- </div>
- <!-- 初始状态 -->
- <div class="initial-state" v-if="false">
- <!-- 快捷操作 -->
- <div class="quick-actions">
- <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn"
- @click="sendQuickAction(action)">{{ action }}</button>
- </div>
- </div>
- <AiWeb :visible="showAiWebModal" :web-id="webId" @close="showAiWebModal = false" @add="addAiWeb" :is-loading="isLoading" />
- </div>
- </template>
- <script lang="ts" setup>
- import { ref, onMounted, useTemplateRef, nextTick, watch } from 'vue'
- import { chat_no_stream, chat_stream, getAgentModel, chat_no_stream2 } from '@/tools/aiChat'
- import { useSlidesStore, useMainStore } from '@/store'
- import { lang } from '@/main'
- import MarkdownIt from 'markdown-it'
- import { getWorkPageId } from '@/services/course'
- import FileInput from '@/components/FileInput2.vue'
- import axios from '@/services/config'
- import message from '@/utils/message'
- import AiWeb from './aiWeb.vue'
- interface ChatMessage {
- uid?: string
- role: 'ai' | 'user'
- content?: string
- aiContent?: string
- oldContent?: string
- loading?: boolean
- chatloading?: boolean
- gLoading?: boolean
- rawContent?: string
- timestamp?: Date
- like?: boolean
- unlike?: boolean
- isTyping?: boolean
- AI?: string
- isShowSynchronization?: boolean
- filename?: string
- is_mind_map?: boolean
- sourceFiles?: Array<{
- title: string
- id?: string
- url?: string
- }>
- jsonData?: {
- gType?: string
- isGenerate?: boolean
- headUrl?: string
- assistantName?: string
- files?: Array<{
- title: string
- id?: string
- url?: string
- }>
- sourceArray?: Array<{
- text?: string
- id?: string
- title?: string
- }>
- aiContent?: string
- }
- }
- const props = withDefaults(defineProps<{
- userid?: string | null
- userJson?: any
- courseTitle?: string | null
- }>(), {
- userid: null,
- userJson: null,
- courseTitle: null,
- })
- const inputText = ref('')
- const messages = ref<ChatMessage[]>([])
- const chatSection = useTemplateRef<HTMLElement>('chatSection')
- const chatLoading = ref(false)
- const showQuickActions = ref(false)
- const streamController = ref<{ abort: () => void } | null>(null)
- const noStreamController = ref<{ promise: Promise<string>; abort: () => void } | null>(null)
- const files = ref<Array<{ title: string; id?: string | null; url?: string; isProcessing?: boolean; cancel?: () => void }>>([])
- // 快捷操作短语数组
- const quickActions = [
- lang.ssAiChatQuickAction1,
- lang.ssAiChatQuickAction2,
- lang.ssAiChatQuickAction6,
- lang.ssAiChatQuickAction7,
- ]
- // 监听输入变化,当输入"/"时显示快捷操作
- watch(inputText, (newValue) => {
- // messages.value.length > 0 &&
- if (newValue === '/') {
- showQuickActions.value = true
- }
- else if (newValue !== '/') {
- showQuickActions.value = false
- }
- })
- const sendMessage = () => {
- if (chatLoading.value) {
- return
- }
- // 检查是否有文件正在处理中
- const hasProcessingFile = files.value.some(file => file.isProcessing)
- if (hasProcessingFile) {
- message.error(lang.ssAiChatWaitUpload)
- return
- }
- if (inputText.value.trim() || files.value.length > 0) {
- // 添加用户消息
- messages.value.push({
- role: 'user',
- content: inputText.value,
- })
- // 模拟AI回复
- // setTimeout(() => {
- // setTimeout(() => {
- // messages.value.at(-1).aiContent = '课程生成完成!为您创建了5个内容页面和3个互动工具。您可以查看底部课程大纲,或在中央区域开始编辑。',
- // messages.value.at(-1).jsonData = {
- // isChoice: true
- // }
- // }, 1000)
- // }, 500)
- prevChatResult()
- messages.value.at(-1).loading = true
- messages.value.at(-1).chatloading = true
- messages.value.at(-1).sourceFiles = files.value.filter(file => file.id !== null).map(file => ({
- title: file.title,
- id: file.id
- }))
- chatLoading.value = true
- sendAction(inputText.value)
- inputText.value = ''
- files.value = []
- }
- }
- const stopMessage = () => {
- if (streamController.value) {
- streamController.value.abort()
- streamController.value = null
- }
- if (noStreamController.value) {
- noStreamController.value.abort()
- noStreamController.value = null
- }
- chatLoading.value = false
- if (messages.value.length > 0) {
- messages.value.at(-1).chatloading = false
- messages.value.at(-1).loading = false
- }
- }
- // 处理文件上传
- const handleFileUpload = async (files2: File[]) => {
- const maxSize = 10 * 1024 * 1024 // 10MB
- const uploadPromises = []
- for (let i = 0; i < files2.length; i++) {
- const file = files2[i]
- if (file.size > maxSize) {
- message.error(lang.ssAiChatFileSizeLimit)
- continue
- }
- // 先添加文件到列表,显示解析中状态
- const fileIndex = files.value.length
- files.value.push({
- title: file.name + ' (' + lang.ssAiChatParsing + ')',
- id: null,
- isProcessing: true,
- cancel: null
- })
- // 创建取消控制器
- const controller = new AbortController()
- files.value[fileIndex].cancel = () => {
- controller.abort()
- files.value.splice(fileIndex, 1)
- }
- // 创建上传Promise并添加到数组
- const uploadPromise = uploadFile2(file, controller.signal).then(res => {
- if (!res) {
- files.value.splice(fileIndex, 1)
- return
- }
- // 上传成功,更新文件状态
- files.value[fileIndex] = {
- title: file.name,
- id: res.results.document_id,
- isProcessing: false
- }
- }).catch(error => {
- if (error.name !== 'AbortError') {
- console.error(lang.ssAiChatUploadFailed, error)
- files.value.splice(fileIndex, 1)
- }
- })
- uploadPromises.push(uploadPromise)
- }
- // 等待所有文件上传完成
- await Promise.allSettled(uploadPromises)
- }
- // 移除文件
- const removeFile = (index: number) => {
- const file = files.value[index]
- if (file && file.isProcessing && file.cancel) {
- file.cancel()
- }
- files.value.splice(index, 1)
- }
- const prevChatResult = () => {
- nextTick(() => {
- if (chatSection.value) {
- chatSection.value.scrollTop = chatSection.value.scrollHeight
- }
- })
- }
- const sendQuickAction = (action: string) => {
- inputText.value = action
- // sendMessage()
- }
- import { v4 as uuidv4 } from 'uuid'
- const session_name = ref('')
- const slidesStore = useSlidesStore()
- const mainStore = useMainStore()
- const gType = ref('chat')
- const sendAction = async (action: string) => {
- const md = new MarkdownIt()
- if (gType.value === 'chat' || gType.value === 'generate_interactive_web' || gType.value === 'trigger_tool_recommend' || gType.value === 'trigger_batch_tool') {
- const result = chat_no_stream(action, agentid1.value, props.userid || '', lang.lang)
- noStreamController.value = result
- console.log(result)
- const content = await result.promise
- if (!content) {
- stopMessage()
- return
- }
- console.log(content)
- // 渲染 Markdown 格式
- try {
- gType.value = JSON.parse(content).intent
- }
- catch (error) {
- gType.value = 'chat'
- }
- // generate_qa // generate_choice_question // generate_photo // generate_poll
- if (gType.value !== 'chat' && gType.value !== 'generate_interactive_web' && gType.value !== 'trigger_batch_tool') {
- messages.value.at(-1).jsonData = {
- gType: gType.value,
- isGenerate: false
- }
- }
- else if (gType.value === 'generate_interactive_web') {
- messages.value.at(-1).jsonData = {
- gType: gType.value,
- isGenerate: false
- }
- messages.value.at(-1).aiContent = lang.ssInteractiveWebWorkspaceDesc
- messages.value.at(-1).loading = false
- messages.value.at(-1).chatloading = false
- chatLoading.value = false
- return
- }
- else if (gType.value === 'trigger_batch_tool') {
- messages.value.at(-1).jsonData = {
- gType: gType.value,
- isGenerate: false
- }
- let isHave = 1
- for (let i = 0; i < slidesStore.slides.length; i++) {
- const slide = slidesStore.slides[i]
- if (slide.toolsArray && slide.toolsArray.length > 0) {
- const hasUnprocessed = slide.toolsArray.length
- if (hasUnprocessed) {
- isHave = 2
- break
- }
- }
- }
- if (isHave == 2) {
- messages.value.at(-1).aiContent = lang.ssBatchInteractiveWebDesc3
- messages.value.at(-1).loading = false
- chatLoading.value = false
- messages.value.at(-1).chatloading = false
- return
- }
-
- messages.value.at(-1).aiContent = lang.ssBatchInteractiveWebDesc
-
- // messages.value.at(-1).loading = false
- // messages.value.at(-1).chatloading = false
- // chatLoading.value = false
- // return
- }
- if (content.includes(lang.ssAiChatQuickGenWebTitle)) {
- gType.value = 'generate_interactive_web'
- messages.value.at(-1).jsonData = {
- gType: 'generate_interactive_web',
- isGenerate: false
- }
- messages.value.at(-1).aiContent = lang.ssInteractiveWebWorkspaceDesc
- messages.value.at(-1).loading = false
- chatLoading.value = false
- messages.value.at(-1).chatloading = false
- return
- }
- if (content.includes(lang.ssAiChatQuickAction5)) {
- gType.value = 'trigger_tool_recommend'
- messages.value.at(-1).jsonData = {
- gType: 'trigger_tool_recommend',
- isGenerate: false
- }
- }
- if (content.includes(lang.ssBatchInteractiveWeb)) {
- gType.value = 'trigger_batch_tool'
- messages.value.at(-1).jsonData = {
- gType: 'trigger_batch_tool',
- isGenerate: false
- }
- let isHave = 1
- for (let i = 0; i < slidesStore.slides.length; i++) {
- const slide = slidesStore.slides[i]
- if (slide.toolsArray && slide.toolsArray.length > 0) {
- const hasUnprocessed = slide.toolsArray.length
- if (hasUnprocessed) {
- isHave = 2
- break
- }
- }
- }
- if (isHave == 2) {
- messages.value.at(-1).aiContent = lang.ssBatchInteractiveWebDesc3
- messages.value.at(-1).loading = false
- chatLoading.value = false
- messages.value.at(-1).chatloading = false
- return
- }
-
- messages.value.at(-1).aiContent = lang.ssBatchInteractiveWebDesc
-
- // messages.value.at(-1).loading = false
- // chatLoading.value = false
- // messages.value.at(-1).chatloading = false
- // return
- }
- }
- else {
- messages.value.at(-1).jsonData = {
- gType: gType.value,
- isGenerate: false
- }
- }
- const getTypeLabel = (type?: number) => {
- const typeMap: Record<number, string> = {
- 45: lang.ssChoiceQ,
- 15: lang.ssQATest,
- 72: lang.ssAiApp,
- 73: lang.ssHPage,
- 81: lang.ssHPage,
- 74: lang.ssVideo,
- 75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
- 76: lang.ssCreative,
- 77: lang.ssEnglishSpeakingTool,
- 78: lang.ssVote,
- 79: lang.ssPhoto,
- }
- return typeMap[type || 0] || lang.ssUnknown
- }
- // 提取所有页面中 type 为 'text' 的元素的纯文本内容
- const allTextContents = slidesStore.slides
- .map((slide: any, index: number) => {
- const elements = slide.elements.filter((element: any) => {
- if (element.type === 'text' || (element.type === 'shape' && element.text && element.text.content)) {
- return true
- }
- // 如果 element.type 包含 frame 且 tooltype 是对应的工具,输出 lang.ssInteract(对应工具)
- if (element.type === 'frame' && element.toolType) {
- const toolLabel = getTypeLabel(element.toolType)
- if (toolLabel !== lang.ssUnknown) {
- return true
- }
- }
- return false
- })
- const pageContents = elements.map((textElement: any) => {
- let content = ''
- if (textElement.type === 'shape') {
- // 创建一个临时元素来解析 HTML 并提取纯文本
- const tempElement = document.createElement('div')
- tempElement.innerHTML = textElement.text.content
- content = tempElement.textContent || tempElement.innerText || ''
- }
- else if (textElement.type === 'frame' && textElement.toolType) {
- // 如果是 frame 类型且有 toolType,输出交互工具标签
- content = lang.ssInteract + `(${getTypeLabel(textElement.toolType)})`
- }
- else {
- // 创建一个临时元素来解析 HTML 并提取纯文本
- const tempElement = document.createElement('div')
- tempElement.innerHTML = textElement.content
- content = tempElement.textContent || tempElement.innerText || ''
- }
- return content
- }).filter((content: string) => content.trim() !== '')
-
- const pageContent = pageContents.length > 0 ? pageContents.join(' ') : '无文本内容'
- return `第${index + 1}页的内容:${pageContent}`
- }) || []
- // 提取当前页面中 type 为 'text' 的元素的纯文本内容
- const textContents = slidesStore.currentSlide?.elements
- .filter((element: any) => element.type === 'text' || (element.type === 'shape' && element.text && element.text.content))
- .map((textElement: any) => {
- if (textElement.type === 'shape') {
- // 创建一个临时元素来解析 HTML 并提取纯文本
- const tempElement = document.createElement('div')
- tempElement.innerHTML = textElement.text.content
- return tempElement.textContent || tempElement.innerText || ''
- }
- // 创建一个临时元素来解析 HTML 并提取纯文本
- const tempElement = document.createElement('div')
- tempElement.innerHTML = textElement.content
- return tempElement.textContent || tempElement.innerText || ''
- })
- .filter(content => content.trim() !== '') || []
- console.log('allTextContents', allTextContents)
- console.log('textContents', textContents)
- let prompt = ''
- let agentid = agentid2.value
- if (gType.value === 'trigger_tool_recommend') {
- agentid = agentid3.value
- prompt = `
- #课程标题(course_title): ${props.courseTitle || ''}
- #当前页面内容(all_pages_content): ${allTextContents.length > 0 ? allTextContents.join('\n') : '无文本内容'}
- #query: ${action}
- `
- }
- else if (gType.value === 'trigger_batch_tool') {
- agentid = agentid4.value
- prompt = `
- #课程标题(course_title): ${props.courseTitle || ''}
- #当前页面内容(all_pages_content): ${allTextContents.length > 0 ? allTextContents.join('\n') : '无文本内容'}
- #query: ${action}
- `
- }
- else {
- prompt = `
- #课程标题(course_title): ${props.courseTitle || ''}
- #当前页面内容(current_page_content): ${textContents.length > 0 ? textContents.join('\n') : '无文本内容'}
- #query: ${action}
- `
- }
- if (gType.value === 'trigger_batch_tool') {
- const result = chat_no_stream(prompt, agentid, props.userid || '', lang.lang)
- streamController.value = result
- console.log(result)
- const content = await result.promise
- messages.value.at(-1).jsonData.aiContent = content
- console.log('event.data', content)
- generate(messages.value.at(-1))
- // chat_stream(prompt, agentid, props.userid || '', lang.lang, async (event) => {
- // if (event.type === 'message') {
- // messages.value.at(-1).jsonData.aiContent = md.render(event.data)
- // messages.value.at(-1).loading = false
-
- // }
- // else if (event.type === 'messageEnd') {
- // messages.value.at(-1).jsonData.aiContent = md.render(event.data)
- // console.log('event.data', event.data)
- // generate(messages.value.at(-1))
- // }
- // }, session_name.value, messages.value.at(-1).sourceFiles?.map(file => file.id).filter(Boolean)).then(controller => {
- // streamController.value = controller
- // }).catch(err => {
- // chatLoading.value = false
- // console.log('err', err)
- // stopMessage()
- // })
- return
- }
-
- chat_stream(prompt, agentid, props.userid || '', lang.lang, (event) => {
- if (event.type === 'message') {
- messages.value.at(-1).aiContent = md.render(event.data)
- messages.value.at(-1).loading = false
- prevChatResult()
- }
- else if (event.type === 'messageEnd') {
- messages.value.at(-1).aiContent = md.render(event.data)
- messages.value.at(-1).chatloading = false
- chatLoading.value = false
- prevChatResult()
- }
- }, session_name.value, messages.value.at(-1).sourceFiles?.map(file => file.id).filter(Boolean)).then(controller => {
- streamController.value = controller
- }).catch(err => {
- chatLoading.value = false
- console.log('err', err)
- stopMessage()
- })
- }
- import useCreateElement from '@/hooks/useCreateElement'
- import useSlideHandler from '@/hooks/useSlideHandler'
- const { createSlide } = useSlideHandler()
- const { createFrameElement } = useCreateElement()
- const setUrl = () => {
- let url = 'https://beta.pbl.cocorobo.cn'
- if (lang.lang === 'cn') {
- url = 'https://pbl.cocorobo.cn/'
- }
- else if (lang.lang === 'hk') {
- url = 'https://pbl.cocorobo.hk/'
- }
- else if (lang.lang === 'en') {
- url = 'https://pbl.cocorobo.com/'
- }
- return url
- }
- const generate = (message: ChatMessage) => {
- if (message.gLoading || message.jsonData?.isGenerate) {
- return
- }
- message.gLoading = true
- if (message.jsonData?.gType === 'generate_choice_question') {
- console.log(message.jsonData?.gType)
- const prompt = [
- {
- role: 'user',
- content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成选择题的json。输出一个json格式的回复,格式如下:{"testCount":1,"testTitle":"","testJson":[{"id":"7de1fdb4-bec3-4324-8986-4623f838e3d7","type":"2","teststitle":"1+1?","checkList":["1","2","3"],"timuList":[],"answer":[1],"userAnswer":[],"explanation":"解析"}]}。`, // 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
- },
- ]
- chat_no_stream2(prompt, { type: 'json_object' }).then(async (res: any) => {
- console.log('选择题', JSON.parse(res))
- gType.value = 'chat'
- setPageId(45, res).then(res => {
- const baseUrl = setUrl()
- const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${45}`
- createSlide()
- createFrameElement(url, 45)
- message.gLoading = false
- message.jsonData.isGenerate = true
- })
- })
- }
- else if (message.jsonData?.gType === 'generate_qa') {
- console.log(message.jsonData?.gType)
- const prompt = [
- {
- role: 'user',
- content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成问答题的json。输出一个json格式的回复,格式如下:{"answerQ":"问题","answer":"","fileList":[],"imageList":[],"evaluationCriteria":"评价标准"}。`, // 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
- },
- ]
- chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
- console.log('问答题', JSON.parse(res))
- gType.value = 'chat'
- setPageId(15, res).then(res => {
- const baseUrl = setUrl()
- const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${15}`
- createSlide()
- createFrameElement(url, 15)
- message.gLoading = false
- message.jsonData.isGenerate = true
- })
- })
- }
- else if (message.jsonData?.gType === 'generate_poll') {
- console.log(message.jsonData?.gType)
- const prompt = [
- {
- role: 'user',
- content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成投票的json。输出一个json格式的回复,格式如下:{"testCount":1,"testTitle":"","testJson":[{"id":"7de1fdb4-bec3-4324-8986-4623f838e3d7","type":"2","teststitle":"1+1?","checkList":["1","2","3"],"timuList":[],"answer":[],"userAnswer":[]]}。`, // 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
- },
- ]
- chat_no_stream2(prompt, { type: 'json_object' }).then(async (res: any) => {
- console.log('投票', JSON.parse(res))
- gType.value = 'chat'
- setPageId(78, res).then(res => {
- const baseUrl = setUrl()
- const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${78}`
- createSlide()
- createFrameElement(url, 78)
- message.gLoading = false
- message.jsonData.isGenerate = true
- })
- })
- }
- else if (message.jsonData?.gType === 'generate_photo') {
- console.log(message.jsonData?.gType)
- const prompt = [
- {
- role: 'user',
- content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成拍照的json。输出一个json格式的回复,格式如下:{"answerQ":"问题","answer":"","fileList":[]}。`, // ,"imageList":[],"evaluationCriteria":"评价标准" 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
- },
- ]
- chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
- console.log('拍照', JSON.parse(res))
- gType.value = 'chat'
- setPageId(79, res).then(res => {
- const baseUrl = setUrl()
- const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${79}`
- createSlide()
- createFrameElement(url, 79)
- message.gLoading = false
- message.jsonData.isGenerate = true
- })
- })
- }
- else if (message.jsonData?.gType === 'trigger_batch_tool') {
- console.log(message.jsonData?.gType)
- const prompt = [
- {
- role: 'user',
- content: `这是用户输入的内容:“${message.jsonData.aiContent}”,根据用户输入的内容,取出里面candidates的值。输出一个json格式的回复`, // ,"imageList":[],"evaluationCriteria":"评价标准" 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
- },
- ]
- chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
- console.log('批量生成工具', JSON.parse(res))
- gType.value = 'chat'
- messages.value.at(-1).chatloading = false
- messages.value.push({
- role: 'ai',
- aiContent: lang.ssBatchInteractiveWebDesc2,
- jsonData: {
- gType: 'trigger_batch_tool'
- }
- })
- chatLoading.value = false
- const array = JSON.parse(res).candidates
- for (let i = 0; i < array.length; i++) {
- const item = array[i]
- const pageIndex = item.page_index - 1
- if (pageIndex >= 0 && pageIndex < slidesStore.slides.length) {
- const slide = slidesStore.slides[pageIndex]
- slidesStore.updateSlide({ toolsArray: item.tools }, slide.id)
- }
- }
-
- const firstPageWithTools = slidesStore.slides.findIndex((slide: any) =>
- slide.toolsArray && slide.toolsArray.length > 0
- )
- if (firstPageWithTools !== -1) {
- slidesStore.updateSlideIndex(firstPageWithTools)
- mainStore.setChildPageState(true, slidesStore.slides[firstPageWithTools].toolsArray || [], firstPageWithTools || 0)
- }
- })
- }
- }
- const setPageId = async (tool: any, json: any) => {
- const res = await getWorkPageId({
- userid: props.userid || '',
- type: tool,
- json: json
- })
- return res[0][0].id
- }
- // 上传文件
- const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
- try {
- const uuid = uuidv4()
- const formData = new FormData()
- const timestamp = Date.now()
- const finalExtension = file.name.split('.').pop()?.toLowerCase() || ''
- const baseName = file.name.slice(0, -(finalExtension.length + 1))
- formData.append(
- 'file',
- new File([file], `${baseName}${timestamp}.${finalExtension}`)
- )
- formData.append('collection_ids', JSON.stringify([]))
- formData.append('id', uuid)
- formData.append('metadata', JSON.stringify({ title: file.name }))
- formData.append('ingestion_mode', 'fast')
- formData.append('run_with_orchestration', 'true')
- // 同步知识库
- const res = await axios.post(
- 'https://r2rserver.cocorobo.cn/v3/documents',
- formData,
- {
- headers: {
- 'Content-Type': 'multipart/form-data',
- },
- signal: signal
- }
- )
- console.log(res)
- return res
- }
- catch (error) {
- console.log('err', error)
- if (error.name === 'AbortError') {
- throw error
- }
- return ''
- }
- }
- const agentid1 = ref('cbb29b41-2a4a-4453-bf8d-357929ced4bd')// 判断意图
- const agentid2 = ref('f86aa63c-b7b7-4d03-9b37-b59f116d36f3')// 生成内容
- const agentid3 = ref('caaa43ab-41a5-466a-94bc-74e0c9265068')// 互动工具推荐
- const agentid4 = ref('b41bc650-9861-468c-a624-b52269dfff22')// 批量生成工具
- const isQuickActions = ref<string[]>([])
- const toggleQuickActions = (section: string) => {
- if (isQuickActions.value.includes(section)) {
- isQuickActions.value = isQuickActions.value.filter((item: string) => item !== section)
- }
- else {
- isQuickActions.value.push(section)
- }
- }
- const cocoQuickTab = ref('page')
- const setCocoQuickTab = (tab: string) => {
- cocoQuickTab.value = tab
- }
- // AI Web 相关状态
- const create_app_loading = ref(false)
- const webId = ref('')
- const showAiWebModal = ref(false)
- const isEdit = ref(false)
- import { storeToRefs } from 'pinia'
- import _ from 'lodash'
- const { currentSlide } = storeToRefs(slidesStore)
- const handle_add_aiWeb = _.throttle(async () => {
- create_app_loading.value = true
- try {
- const elements = currentSlide.value?.elements || []
- console.log(elements)
- const existingWebElement = elements.find((el: any) => el.toolType === 73 && el.webId && el.type === 'frame')
-
- if (existingWebElement && existingWebElement.webId) {
- webId.value = existingWebElement.webId
- create_app_loading.value = false
- showAiWebModal.value = true
- isEdit.value = true
- return
- }
- const res = await axios.post('https://appapi.cocorobo.cn/api/agents/ai_edit', {
- userid: props.userid || '',
- username: props.userJson.name || '',
- muti_name: lang.ssAddWebH5,
- description: lang.ssAddWebH5,
- headUrl: 'https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/default%2F%E9%BB%98%E8%AE%A4%E5%A4%B4%E5%83%8F51741770599274.svg',
- organizeid: props.userJson.organizeid || '',
- content: ''
- })
- console.log(res)
- webId.value = res.id || ''
- create_app_loading.value = false
- showAiWebModal.value = true
- }
- catch (error) {
- console.log(error)
- create_app_loading.value = false
- }
- }, 3000)
- const isLoading = ref(false)
- import useImport from '@/hooks/useImport'
- const { uploadFileToS3 } = useImport()
- const addAiWeb = async (code: string) => {
- if (!code) return
- const file = new File([code], 'index.html', { type: 'text/html' })
- isLoading.value = true
- const url = await uploadFileToS3(file)
- if (isEdit.value) {
- const elements = currentSlide.value?.elements || []
- const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 73))
- if (frameElement) {
- slidesStore.updateElement({
- id: frameElement.id,
- props: { url: url }
- })
- }
- }
- else {
- createSlide()
- createFrameElement(url, 73, webId.value)
- }
-
- isLoading.value = false
- showAiWebModal.value = false
- webId.value = ''
- isEdit.value = false
- }
- onMounted(() => {
- session_name.value = uuidv4()
- getAgentModel(agentid1.value)
- getAgentModel(agentid2.value)
- getAgentModel(agentid3.value)
- getAgentModel(agentid4.value)
- })
- </script>
- <style lang="scss" scoped>
- .ai-chat-container {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- padding: 16px;
- gap: 16px;
- }
- .input-section {
- display: flex;
- flex-direction: column;
- gap: 12px;
- position: relative;
- }
- .input-wrapper {
- position: relative;
- display: flex;
- flex-direction: column;
- background: #fafbfc;
- border: 1.5px solid #e5e7eb;
- border-radius: 8px;
- padding: 8px 12px;
- min-height: 120px;
- }
- .ai-input {
- flex: 1;
- border: none;
- background: transparent;
- font-size: 14px;
- color: #374151;
- outline: none;
- resize: none;
- min-height: 80px;
- &::placeholder {
- color: #9CA3AF;
- }
- }
- .input-actions {
- display: flex;
- // justify-content: space-between;
- align-items: center;
- margin-top: 8px;
- }
- .attach-btn {
- width: 32px;
- height: 32px;
- background: none;
- border: none;
- cursor: pointer;
- padding: 4px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s ease;
- color: #6b7280;
- svg {
- width: 20px;
- height: 20px;
- }
- &:hover {
- background: #FFF4E5;
- color: #F78B22;
- border-radius: 4px;
- }
- input[type="file"] {
- display: none;
- }
- }
- .file-box {
- margin-bottom: 8px;
- min-height: 24px;
- max-height: 70px;
- overflow-y: auto;
- .file-item {
- display: flex;
- align-items: center;
- background: #f5f5f5;
- padding: 4px 8px;
- border-radius: 4px;
- margin-bottom: 4px;
- .file-name {
- flex: 1;
- font-size: 12px;
- color: #374151;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .remove-file-btn {
- background: none;
- border: none;
- cursor: pointer;
- color: #9CA3AF;
- font-size: 12px;
- padding: 2px;
- margin-left: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- svg {
- width: 14px;
- height: 14px;
- }
- &:hover {
- color: #EF4444;
- }
- }
- }
- }
- .send-btn {
- margin-left: auto;
- width: 32px;
- height: 32px;
- border: none;
- background: #FF9300;
- color: #fff;
- border-radius: 50%;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s ease;
- svg {
- width: 20px;
- height: 20px;
- }
- &:hover {
- background: #E68A00;
- }
- &.stop {
- background: unset;
- width: auto;
- svg {
- width: 32px;
- height: 32px;
- }
- }
- }
- .quick-actions {
- // display: flex;
- // flex-direction: column;
- // gap: 8px;
- .quick-action-btn {
- padding: 5px 10px;
- border: 1px solid #f7c58f;
- background: #FFF9F2;
- color: #6b4a1f;
- border-radius: 16px;
- font-size: 12px;
- cursor: pointer;
- text-align: left;
- display: block;
- &:hover {
- border-color: #F78B22;
- background: #FFF4E5;
- color: #111827;
- }
- +.quick-action-btn {
- margin-top: 8px;
- }
- }
- }
- .chat-section {
- // flex: 1;
- height: calc(100% - 155px);
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- // gap: 16px;
- padding-right: 8px;
- &::-webkit-scrollbar {
- width: 6px;
- }
- &::-webkit-scrollbar-track {
- background: #F3F4F6;
- border-radius: 3px;
- }
- &::-webkit-scrollbar-thumb {
- background: #D1D5DB;
- border-radius: 3px;
- &:hover {
- background: #9CA3AF;
- }
- }
- }
- .chat-message {
- max-width: 100%;
- margin-bottom: 10px;
- display: flex;
- flex-direction: column;
- .message-content {
- display: flex;
- flex-direction: column;
- border-radius: 8px;
- padding: 8px 10px;
- font-size: 14px;
- line-height: 1.5;
- color: #374151;
- width: fit-content;
- position: relative;
- +.message-content {
- margin-top: 10px;
- }
- &.loading-state {
- opacity: 0.7;
- cursor: not-allowed;
- pointer-events: none;
- // .submenu-icon {
- // animation: pulse 1s infinite;
- // }
- .spin {
- animation: spin 1s linear infinite;
- }
- .submenu-icon {
- position: absolute;
- width: 100%;
- height: 100%;
- background: #00000026;
- top: 0;
- left: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- svg {
- height: 30px;
- color: #fff;
- }
- }
- }
- }
- }
- .message-content {
- word-break: break-word;
- &.ai-message {
- align-self: flex-start;
- background: #fafbfc;
- border: 1.5px solid #e5e7eb;
- border-bottom-left-radius: 2px;
- &>svg {
- width: 17px;
- height: 17px;
- }
- }
- }
- .message-content {
- &.user-message {
- align-self: flex-end;
- background: #FFF4E5;
- border: 1.5px solid #F78B22;
- border-bottom-right-radius: 2px;
- }
- }
- .initial-state {
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- align-items: flex-start;
- // padding: 24px;
- gap: 16px;
- }
- .confirm-btn {
- margin-top: 10px;
- padding: 6px 15px;
- background: #FF9300;
- color: white;
- border: none;
- border-radius: 8px;
- font-size: 14px;
- cursor: pointer;
- margin-left: auto;
- transition: all 0.3s ease;
- &:hover {
- background: #E68A00;
- }
- &.disabled {
- background: #9CA3AF;
- cursor: not-allowed;
- }
- }
- ul {
- margin: 8px 0;
- padding-left: 20px;
- li {
- margin: 4px 0;
- }
- }
- .quick-actions-popup {
- position: absolute;
- bottom: 100%;
- left: 0;
- right: 0;
- background: white;
- border: 1px solid #E5E7EB;
- border-radius: 8px;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
- padding: 8px;
- z-index: 100;
- margin-bottom: 8px;
- .quick-action-btn {
- width: 100%;
- text-align: left;
- padding: 10px 12px;
- background: white;
- border: none;
- border-radius: 6px;
- font-size: 14px;
- color: #374151;
- cursor: pointer;
- transition: all 0.2s ease;
- &:hover {
- background: #fff4e5;
- }
- }
- }
- .message-quick-box {
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
- gap: 10px;
- .message-quick-box-item {
- box-shadow: rgba(17, 24, 39, 0.04) 0px 10px 30px;
- border-width: 1px;
- border-style: solid;
- border-color: rgb(239, 229, 216);
- border-image: initial;
- border-radius: 18px;
- background: rgba(255, 255, 255, 0.96);
- overflow: hidden;
- .coco-collapse-trigger {
- width: 100%;
- display: flex;
- align-items: center;
- cursor: pointer;
- text-align: left;
- gap: 12px;
- border-width: initial;
- border-style: none;
- border-color: initial;
- border-image: initial;
- background: transparent;
- padding: 12px 14px;
- &:hover {
- background: rgb(255, 251, 245);
- }
- }
- .coco-collapse-icon.soft-orange {
- color: rgb(154, 91, 17);
- background: rgb(255, 240, 219);
- }
- .coco-collapse-icon {
- width: 34px;
- height: 34px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- border-radius: 12px;
- }
- .coco-collapse-copy {
- min-width: 0px;
- display: flex;
- flex-direction: column;
- flex: 1 1 0%;
- gap: 2px;
- .coco-collapse-title {
- font-size: 14px;
- font-weight: 700;
- color: rgb(17, 24, 39);
- }
- .coco-collapse-subtitle {
- font-size: 12px;
- color: rgb(139, 115, 86);
- line-height: 1.5;
- }
- }
- .coco-collapse-chevron {
- color: rgb(156, 163, 175);
- flex-shrink: 0;
- svg {
- transition: transform 0.2s;
- }
- }
- &.active {
- .coco-collapse-chevron svg {
- transform: rotate(180deg);
- }
- }
- .coco-collapse-body {
- padding: 0px 14px 14px;
- .coco-quick-tabs {
- display: grid;
- grid-template-columns: repeat(2, minmax(0px, 1fr));
- margin-bottom: 12px;
- gap: 8px;
- .coco-quick-tab {
- height: 34px;
- color: rgb(124, 92, 56);
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- border-radius: 12px;
- border-width: 1px;
- border-style: solid;
- border-color: rgb(234, 223, 206);
- border-image: initial;
- background: rgb(250, 247, 242);
- transition: 0.2s;
- &.active {
- color: rgb(154, 91, 17);
- box-shadow: rgba(247, 139, 34, 0.12) 0px 8px 18px;
- border-color: rgba(247, 139, 34, 0.48);
- background: rgb(255, 244, 229);
- }
- }
- }
- .coco-quick-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
- .coco-quick-action {
- width: 100%;
- display: flex;
- align-items: flex-start;
- cursor: pointer;
- text-align: left;
- gap: 12px;
- border-width: 1px;
- border-style: solid;
- border-color: rgb(239, 229, 216);
- border-image: initial;
- background: rgb(255, 253, 250);
- border-radius: 14px;
- padding: 12px;
- transition: 0.2s;
- .coco-quick-action-copy {
- min-width: 0px;
- display: flex;
- flex-direction: column;
- flex: 1 1 0%;
- gap: 4px;
- .coco-quick-action-title {
- font-size: 13px;
- font-weight: 700;
- color: rgb(17, 24, 39);
- line-height: 1.4;
- }
- .coco-quick-action-desc {
- font-size: 12px;
- line-height: 1.6;
- color: rgb(139, 115, 86);
- }
- }
- .coco-quick-action-arrow {
- color: rgb(212, 163, 115);
- flex-shrink: 0;
- padding-top: 2px;
- }
- }
- }
- }
- }
- }
- .coco-interactive-web-card {
- width: 100%;
- margin-top: 10px;
- display: flex;
- flex-direction: column;
- text-align: left;
- cursor: pointer;
- border-width: 1px;
- border-style: solid;
- border-color: rgb(247, 197, 143);
- border-image: initial;
- border-radius: 14px;
- background: linear-gradient(rgb(255, 250, 243) 0%, rgb(255, 255, 255) 100%);
- padding: 12px 14px;
- gap: 8px;
- transition: 0.2s;
- position: relative;
- overflow: hidden;
- &:hover {
- box-shadow: rgba(247, 139, 34, 0.1) 0px 10px 24px;
- transform: translateY(-1px);
- border-color: rgb(247, 139, 34);
- }
- &.loading-state {
- opacity: 0.7;
- cursor: not-allowed;
- pointer-events: none;
- // .submenu-icon {
- // animation: pulse 1s infinite;
- // }
- .spin {
- animation: spin 1s linear infinite;
- }
- .submenu-icon {
- position: absolute;
- width: 100%;
- height: 100%;
- background: #00000026;
- top: 0;
- left: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- svg {
- height: 30px;
- color: #fff;
- }
- }
- }
- .coco-interactive-web-card-head,
- .coco-interactive-web-card-foot {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- .coco-interactive-web-card-title {
- min-width: 0px;
- font-size: 14px;
- font-weight: 700;
- color: rgb(17, 24, 39);
- line-height: 1.4;
- flex: 1 1 0%;
- }
- .coco-interactive-web-card-action {
- margin-left: auto;
- font-size: 12px;
- font-weight: 700;
- color: rgb(240, 120, 21);
- }
- }
- .coco-interactive-web-card-desc {
- font-size: 12px;
- color: rgb(107, 114, 128);
- line-height: 1.6;
- }
- }
- </style>
- <style>
- .chat table {
- text-align: center;
- border-spacing: 0;
- border-left: 1px solid #000;
- border-bottom: 1px solid #000;
- }
- .chat table td,
- .chat table th {
- border-top: 1px solid #000;
- border-right: 1px solid #000;
- padding: 10px;
- }
- .message-files {
- margin-top: 8px;
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
- .message-file-item {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- color: #6b7280;
- background: #f3f4f6;
- padding: 4px 8px;
- border-radius: 4px;
- max-width: 200px;
- overflow: hidden;
- }
- .message-file-item span {
- flex: 1;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- </style>
|