|
|
@@ -101,11 +101,79 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div v-if="message.content" class="message-text" v-html="renderMarkdown(message.content, tableIcons)"></div>
|
|
|
+ <div
|
|
|
+ v-if="message.content"
|
|
|
+ class="message-text"
|
|
|
+ v-html="
|
|
|
+ renderMarkdown(
|
|
|
+ message.content,
|
|
|
+ tableIcons,
|
|
|
+ message.citations?.items?.length ? Math.max(...message.citations.items.map((i) => i.id)) : undefined
|
|
|
+ )
|
|
|
+ "
|
|
|
+ @click="handleFootnoteClick($event, index)"
|
|
|
+ ></div>
|
|
|
+
|
|
|
+ <!-- 数据来源卡片 citations -->
|
|
|
+ <div v-if="message.citations" class="citations-card">
|
|
|
+ <div v-if="message.citations.model_only" class="citations-header citations-header-simple">
|
|
|
+ <span class="citations-title">本回答依据模型知识生成,仅供参考</span>
|
|
|
+ </div>
|
|
|
+ <template v-else-if="message.citations.items?.length">
|
|
|
+ <div class="citations-header" @click="toggleCitations(index)">
|
|
|
+ <div class="citations-header-left">
|
|
|
+ <span class="citations-title">数据来源</span>
|
|
|
+ <span class="citations-count">{{ message.citations.items.length }}</span>
|
|
|
+ </div>
|
|
|
+ <div :class="['thinking-arrow', { expanded: !collapsedCitations[index] }]">
|
|
|
+ <img :src="getCssUrl('--img-chat-arrow-icon')" width="10" height="10" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-show="!collapsedCitations[index]" class="citations-body">
|
|
|
+ <div v-for="item in message.citations.items" :key="item.id" :id="`citation-${index}-${item.id}`" class="citation-item">
|
|
|
+ <div class="citation-item-row">
|
|
|
+ <span :class="['citation-badge', `citation-badge-${item.type}`]">{{ item.type_cn }}</span>
|
|
|
+ <span class="citation-title-text">{{ item.title }}</span>
|
|
|
+ <span v-if="item.search_url" class="citation-link">知识库检索</span>
|
|
|
+ <span v-if="item.url" class="citation-link">链接</span>
|
|
|
+ </div>
|
|
|
+ <div class="citation-item-attrs">
|
|
|
+ <span v-if="item.source_system" class="citation-attr">{{ item.source_system }}</span>
|
|
|
+ <span v-if="item.fetched_at" class="citation-attr">{{ item.fetched_at }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="item.snippet" class="citation-snippet" :title="item.snippet">{{ item.snippet }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+
|
|
|
<div v-if="message.wordDownloadUrl" class="word-download-area">
|
|
|
<a-button type="link" @click="emit('download-word', message.wordDownloadUrl)"> 下载报告文件 (.docx) </a-button>
|
|
|
</div>
|
|
|
- <div v-if="message.isLoading" class="loading-dots">
|
|
|
+
|
|
|
+ <!-- 文件操作卡片 -->
|
|
|
+ <div v-if="message.downloadFiles?.length" class="download-file-cards">
|
|
|
+ <div v-for="(file, fIdx) in message.downloadFiles" :key="fIdx" class="download-file-card">
|
|
|
+ <div class="file-card-info">
|
|
|
+ <div class="file-card-icon"></div>
|
|
|
+ <div class="file-card-text">
|
|
|
+ <span class="file-card-name">{{ file.filename }}</span>
|
|
|
+ <span class="file-card-hint">点击操作文件</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="file-card-actions">
|
|
|
+ <button class="file-action-btn download-btn" @click="handleDownloadFile(file.url, file.filename)">
|
|
|
+ <img :src="getCssUrl('--img-chat-download-icon-btn')" width="14" height="14" />
|
|
|
+ <span>下载</span>
|
|
|
+ </button>
|
|
|
+ <button v-if="isEditableFile(file.filename)" class="file-action-btn edit-btn" @click="handleEditFile(file.url, file.filename)">
|
|
|
+ <img :src="getCssUrl('--img-chat-edit-icon')" width="14" height="14" />
|
|
|
+ <span>编辑</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-show="message.isLoading" class="loading-dots">
|
|
|
<span class="dot"></span>
|
|
|
<span class="dot"></span>
|
|
|
<span class="dot"></span>
|
|
|
@@ -128,31 +196,47 @@
|
|
|
<div
|
|
|
v-for="(_, sIdx) in message.pendingApprovalData.ask_user.questions"
|
|
|
:key="sIdx"
|
|
|
- :class="['ask-user-step-dot', {
|
|
|
- active: sIdx === (askUserCurrentStep[index] ?? 0),
|
|
|
- completed: sIdx < (askUserCurrentStep[index] ?? 0) || !!(askUserAnswers[index]?.[sIdx] && askUserAnswers[index][sIdx].trim())
|
|
|
- }]"
|
|
|
+ :class="[
|
|
|
+ 'ask-user-step-dot',
|
|
|
+ {
|
|
|
+ active: sIdx === (askUserCurrentStep[index] ?? 0),
|
|
|
+ completed: sIdx < (askUserCurrentStep[index] ?? 0) || !!(askUserAnswers[index]?.[sIdx] && askUserAnswers[index][sIdx].trim()),
|
|
|
+ },
|
|
|
+ ]"
|
|
|
@click="askUserCurrentStep[index] = sIdx"
|
|
|
></div>
|
|
|
- <span class="ask-user-step-text">{{ (askUserCurrentStep[index] ?? 0) + 1 }} / {{ message.pendingApprovalData.ask_user.questions.length }}</span>
|
|
|
+ <span class="ask-user-step-text"
|
|
|
+ >{{ (askUserCurrentStep[index] ?? 0) + 1 }} / {{ message.pendingApprovalData.ask_user.questions.length }}</span
|
|
|
+ >
|
|
|
</div>
|
|
|
|
|
|
<!-- 当前问题 -->
|
|
|
- <div
|
|
|
- v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0]"
|
|
|
- class="ask-user-question"
|
|
|
- >
|
|
|
+ <div v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0]" class="ask-user-question">
|
|
|
<div class="ask-user-question-text">
|
|
|
{{ message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].question }}
|
|
|
- <span v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].required !== false" class="ask-user-required">*</span>
|
|
|
+ <span
|
|
|
+ v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].required !== false"
|
|
|
+ class="ask-user-required"
|
|
|
+ >*</span
|
|
|
+ >
|
|
|
</div>
|
|
|
|
|
|
<!-- multiple_choice 类型 -->
|
|
|
- <div v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].type === 'multiple_choice'" class="ask-user-choices">
|
|
|
+ <div
|
|
|
+ v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].type === 'multiple_choice'"
|
|
|
+ class="ask-user-choices"
|
|
|
+ >
|
|
|
<div
|
|
|
v-for="(choice, cIdx) in message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].choices"
|
|
|
:key="cIdx"
|
|
|
- :class="['ask-user-choice', { selected: askUserAnswers[index]?.[askUserCurrentStep[index] ?? 0] === getChoiceValue(choice) && !askUserOtherSelected[index]?.[askUserCurrentStep[index] ?? 0] }]"
|
|
|
+ :class="[
|
|
|
+ 'ask-user-choice',
|
|
|
+ {
|
|
|
+ selected:
|
|
|
+ askUserAnswers[index]?.[askUserCurrentStep[index] ?? 0] === getChoiceValue(choice) &&
|
|
|
+ !askUserOtherSelected[index]?.[askUserCurrentStep[index] ?? 0],
|
|
|
+ },
|
|
|
+ ]"
|
|
|
@click="selectChoice(index, askUserCurrentStep[index] ?? 0, choice, message.pendingApprovalData.ask_user.questions.length)"
|
|
|
>
|
|
|
<span>{{ getChoiceValue(choice) }}</span>
|
|
|
@@ -174,7 +258,10 @@
|
|
|
</div>
|
|
|
|
|
|
<!-- text 类型 -->
|
|
|
- <div v-else-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].type === 'text'" class="ask-user-text">
|
|
|
+ <div
|
|
|
+ v-else-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].type === 'text'"
|
|
|
+ class="ask-user-text"
|
|
|
+ >
|
|
|
<textarea
|
|
|
class="ask-user-textarea"
|
|
|
placeholder="请输入..."
|
|
|
@@ -186,23 +273,33 @@
|
|
|
|
|
|
<!-- 操作按钮 -->
|
|
|
<div class="approval-actions">
|
|
|
- <button
|
|
|
- v-if="(askUserCurrentStep[index] ?? 0) > 0"
|
|
|
- class="approval-btn nav-btn"
|
|
|
- @click="goPrevStep(index)"
|
|
|
- >上一步</button>
|
|
|
+ <button v-if="(askUserCurrentStep[index] ?? 0) > 0" class="approval-btn nav-btn" @click="goPrevStep(index)">上一步</button>
|
|
|
<button
|
|
|
v-if="(askUserCurrentStep[index] ?? 0) < message.pendingApprovalData.ask_user.questions.length - 1"
|
|
|
class="approval-btn approve-btn"
|
|
|
- :disabled="!isCurrentStepAnswered(index, askUserCurrentStep[index] ?? 0, message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0])"
|
|
|
+ :disabled="
|
|
|
+ !isCurrentStepAnswered(
|
|
|
+ index,
|
|
|
+ askUserCurrentStep[index] ?? 0,
|
|
|
+ message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0]
|
|
|
+ )
|
|
|
+ "
|
|
|
@click="goNextStep(index, message.pendingApprovalData.ask_user.questions.length)"
|
|
|
- >下一步</button>
|
|
|
+ >下一步</button
|
|
|
+ >
|
|
|
<button
|
|
|
v-if="(askUserCurrentStep[index] ?? 0) === message.pendingApprovalData.ask_user.questions.length - 1"
|
|
|
class="approval-btn approve-btn"
|
|
|
- :disabled="!isCurrentStepAnswered(index, askUserCurrentStep[index] ?? 0, message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0])"
|
|
|
+ :disabled="
|
|
|
+ !isCurrentStepAnswered(
|
|
|
+ index,
|
|
|
+ askUserCurrentStep[index] ?? 0,
|
|
|
+ message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0]
|
|
|
+ )
|
|
|
+ "
|
|
|
@click="handleSubmitAskUser(index)"
|
|
|
- >提交</button>
|
|
|
+ >提交</button
|
|
|
+ >
|
|
|
<button class="approval-btn reject-btn" @click="handleCancelAskUser">取消</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -263,7 +360,9 @@
|
|
|
<script setup lang="ts">
|
|
|
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
|
|
import dayjs from 'dayjs';
|
|
|
- import { renderMarkdown, formatFileSize, tableHtmlToMarkdown, tableHtmlToCsv } from './utils';
|
|
|
+ import { renderMarkdown, formatFileSize, tableHtmlToMarkdown, tableHtmlToCsv, isMdFile, isTextFile } from './utils';
|
|
|
+ import { getToken } from '/@/utils/auth';
|
|
|
+ import { message } from 'ant-design-vue';
|
|
|
import type { Message, AttachedFile, ThinkingStep } from './types';
|
|
|
// import katex from 'katex';
|
|
|
// import 'katex/dist/katex.min.css';
|
|
|
@@ -336,10 +435,53 @@
|
|
|
const emit = defineEmits<{
|
|
|
(e: 'file-preview', file: AttachedFile): void;
|
|
|
(e: 'download-word', url: string): void;
|
|
|
+ (e: 'open-file-preview', file: AttachedFile): void;
|
|
|
(e: 'ask-user-submit', answers: string[]): void;
|
|
|
(e: 'ask-user-cancel'): void;
|
|
|
}>();
|
|
|
|
|
|
+ const isEditableFile = (filename: string) => isMdFile(filename) || isTextFile(filename);
|
|
|
+
|
|
|
+ const handleDownloadFile = (url: string, filename: string) => {
|
|
|
+ if (!url) return;
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = url;
|
|
|
+ a.download = filename;
|
|
|
+ document.body.appendChild(a);
|
|
|
+ a.click();
|
|
|
+ document.body.removeChild(a);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleEditFile = async (url: string, filename: string) => {
|
|
|
+ try {
|
|
|
+ const resp = await fetch(url, { headers: { 'X-Access-Token': getToken() } });
|
|
|
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
|
+ const content = await resp.text();
|
|
|
+ const ext = filename.split('.').pop()?.toLowerCase() || '';
|
|
|
+ const mimeMap: Record<string, string> = {
|
|
|
+ md: 'text/markdown',
|
|
|
+ txt: 'text/plain',
|
|
|
+ json: 'application/json',
|
|
|
+ xml: 'text/xml',
|
|
|
+ yaml: 'text/yaml',
|
|
|
+ yml: 'text/yaml',
|
|
|
+ csv: 'text/csv',
|
|
|
+ };
|
|
|
+ const file: AttachedFile = {
|
|
|
+ id: `report-${Date.now()}`,
|
|
|
+ name: filename,
|
|
|
+ size: content.length,
|
|
|
+ type: mimeMap[ext] || 'text/plain',
|
|
|
+ content,
|
|
|
+ uploadTime: new Date().toISOString(),
|
|
|
+ };
|
|
|
+ emit('open-file-preview', file);
|
|
|
+ } catch (err) {
|
|
|
+ console.error('获取文件内容失败:', err);
|
|
|
+ message.error('获取文件内容失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
// ask_user 答案状态(按消息索引存储)
|
|
|
const askUserAnswers = reactive<Record<number, string[]>>({});
|
|
|
const askUserOtherSelected = reactive<Record<number, boolean[]>>({});
|
|
|
@@ -408,6 +550,7 @@
|
|
|
const collapsedThinking = reactive<Record<number, boolean>>({});
|
|
|
const collapsedThinkingContent = reactive<Record<number, boolean>>({});
|
|
|
const expandedAgents = reactive<Record<string, boolean>>({});
|
|
|
+ const collapsedCitations = reactive<Record<number, boolean>>({});
|
|
|
const isAtBottom = ref(true);
|
|
|
const copiedMessageIndex = ref(-1);
|
|
|
|
|
|
@@ -495,14 +638,18 @@
|
|
|
const scrollToBottom = () => {
|
|
|
const el = messagesRef.value;
|
|
|
if (el) {
|
|
|
- el.scrollTop = el.scrollHeight;
|
|
|
+ el.scrollTop = el.scrollHeight - el.clientHeight;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const forceScrollToBottom = () => {
|
|
|
const el = messagesRef.value;
|
|
|
if (el) {
|
|
|
- el.scrollTop = el.scrollHeight;
|
|
|
+ el.scrollTop = el.scrollHeight - el.clientHeight;
|
|
|
+ // 使用 requestAnimationFrame 确保 DOM 渲染完成后再滚动一次
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ el.scrollTop = el.scrollHeight - el.clientHeight;
|
|
|
+ });
|
|
|
}
|
|
|
isAtBottom.value = true;
|
|
|
};
|
|
|
@@ -536,6 +683,23 @@
|
|
|
expandedAgents[key] = !expandedAgents[key];
|
|
|
};
|
|
|
|
|
|
+ const toggleCitations = (index: number) => {
|
|
|
+ collapsedCitations[index] = !collapsedCitations[index];
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleFootnoteClick = (e: Event, msgIndex: number) => {
|
|
|
+ const target = (e.target as HTMLElement).closest('.footnote-ref') as HTMLElement;
|
|
|
+ if (!target) return;
|
|
|
+ const n = target.dataset.n;
|
|
|
+ if (!n) return;
|
|
|
+ const el = document.getElementById(`citation-${msgIndex}-${n}`);
|
|
|
+ if (el) {
|
|
|
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
+ el.classList.add('citation-highlight');
|
|
|
+ setTimeout(() => el.classList.remove('citation-highlight'), 1500);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
const getLatestStepMessage = (group: AgentGroup): string => {
|
|
|
if (group.steps.length === 0) return '';
|
|
|
const last = group.steps[group.steps.length - 1];
|
|
|
@@ -645,7 +809,7 @@
|
|
|
.message-body {
|
|
|
padding: 12px 12px 4px 12px;
|
|
|
border-radius: 6px;
|
|
|
- width: calc(100% - 50px);
|
|
|
+ width: 100%;
|
|
|
min-width: 0;
|
|
|
overflow: hidden;
|
|
|
|
|
|
@@ -1011,6 +1175,7 @@
|
|
|
align-items: center;
|
|
|
gap: 6px;
|
|
|
padding: 4px 0;
|
|
|
+ min-height: 16px;
|
|
|
|
|
|
.dot {
|
|
|
width: 8px;
|
|
|
@@ -1269,6 +1434,270 @@
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ .citations-card {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .citations-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 8px 14px;
|
|
|
+ border-bottom: 1px solid rgba(63, 80, 106, 0.3);
|
|
|
+ cursor: pointer;
|
|
|
+ user-select: none;
|
|
|
+ transition: background 0.2s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: rgba(10, 132, 255, 0.08);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.citations-header-simple {
|
|
|
+ cursor: default;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: transparent;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .citations-header-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .citations-title {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #8b949e;
|
|
|
+ }
|
|
|
+
|
|
|
+ .citations-count {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #8b949e;
|
|
|
+ background: rgba(10, 132, 255, 0.1);
|
|
|
+ padding: 1px 6px;
|
|
|
+ border-radius: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .thinking-arrow {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ transition: transform 0.2s;
|
|
|
+
|
|
|
+ &.expanded {
|
|
|
+ transform: rotate(180deg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .citations-body {
|
|
|
+ padding: 4px 14px;
|
|
|
+
|
|
|
+ .citation-item {
|
|
|
+ padding: 6px 0;
|
|
|
+ border-bottom: 1px solid rgba(63, 80, 106, 0.15);
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.citation-highlight {
|
|
|
+ background: rgba(10, 132, 255, 0.08);
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 6px 8px;
|
|
|
+ margin: 0 -8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .citation-item-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ line-height: 1.5;
|
|
|
+
|
|
|
+ .citation-badge {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #79c0ff;
|
|
|
+ font-family: monospace;
|
|
|
+ background: rgba(10, 132, 255, 0.1);
|
|
|
+ padding: 1px 5px;
|
|
|
+ border-radius: 3px;
|
|
|
+ white-space: nowrap;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .citation-badge-knowledge_base {
|
|
|
+ color: #3fb950;
|
|
|
+ background: rgba(46, 160, 67, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .citation-badge-web {
|
|
|
+ color: #d29922;
|
|
|
+ background: rgba(210, 153, 34, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .citation-badge-calc {
|
|
|
+ color: #a371f7;
|
|
|
+ background: rgba(163, 113, 247, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .citation-title-text {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #b0b8c4;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .citation-link {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #0a84ff;
|
|
|
+ text-decoration: none;
|
|
|
+ white-space: nowrap;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .citation-item-attrs {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 4px 10px;
|
|
|
+ padding-left: 0;
|
|
|
+ margin-top: 2px;
|
|
|
+
|
|
|
+ .citation-attr {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #8b949e;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .citation-low-score {
|
|
|
+ color: #d29922;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .citation-snippet {
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #8b949e;
|
|
|
+ line-height: 1.5;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .download-file-cards {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ margin-top: 12px;
|
|
|
+
|
|
|
+ .download-file-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 12px 16px;
|
|
|
+ background: linear-gradient(135deg, rgba(0, 47, 74, 0.8) 0%, rgba(0, 60, 90, 0.6) 100%);
|
|
|
+ border: 1px solid rgba(0, 84, 130, 0.6);
|
|
|
+ border-radius: 10px;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: rgba(10, 132, 255, 0.4);
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-card-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ min-width: 0;
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ .file-card-icon {
|
|
|
+ width: 36px;
|
|
|
+ height: 36px;
|
|
|
+ background-image: var(--img-chat-download-icon);
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ flex-shrink: 0;
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-card-text {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+ min-width: 0;
|
|
|
+
|
|
|
+ .file-card-name {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #e0e6ed;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-card-hint {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #6e7681;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-card-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ .file-action-btn {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+ padding: 6px 14px;
|
|
|
+ border-radius: 7px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+ cursor: pointer;
|
|
|
+ border: none;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ white-space: nowrap;
|
|
|
+
|
|
|
+ &.download-btn {
|
|
|
+ background: rgba(255, 255, 255, 0.08);
|
|
|
+ color: #c9d1d9;
|
|
|
+ border: 1px solid rgba(63, 80, 106, 0.5);
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.14);
|
|
|
+ color: #e0e6ed;
|
|
|
+ border-color: rgba(63, 80, 106, 0.8);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.edit-btn {
|
|
|
+ background: linear-gradient(135deg, #0a84ff 0%, #0066cc 100%);
|
|
|
+ color: #fff;
|
|
|
+ border: 1px solid rgba(10, 132, 255, 0.5);
|
|
|
+ box-shadow: 0 2px 6px rgba(10, 132, 255, 0.2);
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: linear-gradient(135deg, #3a9bfd 0%, #0a84ff 100%);
|
|
|
+ box-shadow: 0 3px 10px rgba(10, 132, 255, 0.35);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.user-message {
|