forked from b2894lxlx/tuanwei-admin
feat(ai): 助手页支持写作/生图等记录的历史列表与按类型筛选
- 三模式各自独立工作区:切模式不丢消息、草稿与当前记录 - 历史面板加类型徽标与筛选(全部/聊天/写作/图片/创意/活动策划),文案改「历史记录」 - 生成成功认领末帧 chatId 并刷新列表;聊天 __END__ 帧的 chatId 同样接受 - 生图回复支持多张  渲染 - 写作/图片记录支持「按原参数再来一次」(只回填、不自动发送) - 三个独立页(文稿/创意/活动策划)加历史入口,/ai?type= 预置筛选 - 后台 aiChatList 加类型列与筛选;历史列表提示仅展示最近 30 条
This commit is contained in:
@@ -28,6 +28,10 @@ export interface AiChatList {
|
||||
updateTime?: string;
|
||||
//
|
||||
title?: string;
|
||||
// 记录类型:chat / writing / image / creative / activity_outline
|
||||
chatType?: string;
|
||||
// 生成记录入参快照(JSON 字符串,可能为 null)
|
||||
params?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,5 +47,6 @@ export interface AiChatListParam extends PageParam {
|
||||
status?: number;
|
||||
deleted?: number;
|
||||
title?: string;
|
||||
chatType?: string;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* AI 历史记录的类型(对应后端 `ai_chat_list.chat_type`)。
|
||||
*
|
||||
* 契约见 `docs/ai-history-contract.md`:类型由后端在生成成功时判定,前端只负责展示与筛选。
|
||||
* 旧数据没有 `chat_type`,按聊天处理。
|
||||
*/
|
||||
|
||||
export type AiChatType =
|
||||
| 'chat'
|
||||
| 'writing'
|
||||
| 'image'
|
||||
| 'creative'
|
||||
| 'activity_outline';
|
||||
|
||||
/** 类型的中文名(徽标与后台表格共用) */
|
||||
export const AI_CHAT_TYPE_LABELS: Record<AiChatType, string> = {
|
||||
chat: '聊天',
|
||||
writing: '写作',
|
||||
image: '图片',
|
||||
creative: '创意',
|
||||
activity_outline: '活动策划'
|
||||
};
|
||||
|
||||
/** 筛选下拉的可选项(不含「全部」,由使用方按需补充) */
|
||||
export const AI_CHAT_TYPE_OPTIONS: { label: string; value: AiChatType }[] = [
|
||||
{ label: '聊天', value: 'chat' },
|
||||
{ label: '写作', value: 'writing' },
|
||||
{ label: '图片', value: 'image' },
|
||||
{ label: '创意', value: 'creative' },
|
||||
{ label: '活动策划', value: 'activity_outline' }
|
||||
];
|
||||
|
||||
/** 把后端返回值收敛到已知类型,缺失或未知一律按聊天处理 */
|
||||
export function resolveAiChatType(value?: string | null): AiChatType {
|
||||
const type = String(value || '').trim() as AiChatType;
|
||||
return type in AI_CHAT_TYPE_LABELS ? type : 'chat';
|
||||
}
|
||||
|
||||
/** 直接取中文名,用于模板与表格列 */
|
||||
export function getAiChatTypeLabel(value?: string | null): string {
|
||||
return AI_CHAT_TYPE_LABELS[resolveAiChatType(value)];
|
||||
}
|
||||
+24
-2
@@ -148,6 +148,28 @@ export function streamCreativeAssistant(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 末帧 `result` 是普通接口的完整响应包(`{code, message, data}`)。
|
||||
* 生成成功后后端会在包里带上 `chatId`(见 `docs/ai-history-contract.md`),
|
||||
* 既可能在包上也可能在 `data` 里;这里统一把它合并进返回的 data,
|
||||
* 页面只读 `result.chatId` 即可。
|
||||
*/
|
||||
function unwrapFinalData(finalResult: any) {
|
||||
const data = finalResult?.data ?? null;
|
||||
const chatId = Number(finalResult?.chatId);
|
||||
if (
|
||||
data &&
|
||||
typeof data === 'object' &&
|
||||
!Array.isArray(data) &&
|
||||
Number.isFinite(chatId) &&
|
||||
chatId > 0 &&
|
||||
data.chatId == null
|
||||
) {
|
||||
data.chatId = chatId;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创意助手(出图/出视频):跑完才有结果,中途一个字节都不推。
|
||||
*
|
||||
@@ -181,7 +203,7 @@ export async function runCreativeAssistantStream(
|
||||
if (Number(finalResult.code) !== 0) {
|
||||
throw new Error(finalResult.message || '创意助手处理失败');
|
||||
}
|
||||
return finalResult.data ?? null;
|
||||
return unwrapFinalData(finalResult);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,5 +246,5 @@ export async function runManuscriptGenStream(
|
||||
if (Number(finalResult.code) !== 0) {
|
||||
throw new Error(finalResult.message || '稿件生成失败');
|
||||
}
|
||||
return finalResult.data ?? null;
|
||||
return unwrapFinalData(finalResult);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-select
|
||||
v-model:value="where.chatType"
|
||||
allow-clear
|
||||
placeholder="请选择类型"
|
||||
style="width: 180px"
|
||||
:options="chatTypeOptions"
|
||||
@change="search"
|
||||
/>
|
||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
@@ -12,8 +20,9 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { GradeParam } from '@/api/user/grade/model';
|
||||
import { watch } from 'vue';
|
||||
import type { AiChatListParam } from '@/api/ai/aiChatList/model';
|
||||
import { AI_CHAT_TYPE_OPTIONS } from '@/utils/ai-chat-type';
|
||||
import { reactive, watch } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -24,12 +33,26 @@
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: GradeParam): void;
|
||||
(e: 'search', where?: AiChatListParam): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'remove'): void;
|
||||
(e: 'batchMove'): void;
|
||||
}>();
|
||||
|
||||
const chatTypeOptions = AI_CHAT_TYPE_OPTIONS;
|
||||
|
||||
const where = reactive<AiChatListParam>({
|
||||
chatType: undefined
|
||||
});
|
||||
|
||||
// 类型筛选:清空即回到全部
|
||||
const search = () => {
|
||||
emit('search', {
|
||||
...where,
|
||||
chatType: where.chatType || undefined
|
||||
});
|
||||
};
|
||||
|
||||
// 新增
|
||||
const add = () => {
|
||||
emit('add');
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'chatType'">
|
||||
<a-tag :color="chatTypeColor(record.chatType)">
|
||||
{{ getAiChatTypeLabel(record.chatType) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 0" color="red">隐藏</a-tag>
|
||||
@@ -64,6 +69,7 @@
|
||||
import AiChatListEdit from './components/aiChatListEdit.vue';
|
||||
import { pageAiChatList, removeAiChatList, removeBatchAiChatList } from '@/api/ai/aiChatList';
|
||||
import type { AiChatList, AiChatListParam } from '@/api/ai/aiChatList/model';
|
||||
import { getAiChatTypeLabel, resolveAiChatType } from '@/utils/ai-chat-type';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
@@ -79,6 +85,22 @@
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 记录类型徽标的颜色,与助手页历史面板一致
|
||||
const chatTypeColor = (value?: string) => {
|
||||
switch (resolveAiChatType(value)) {
|
||||
case 'writing':
|
||||
return 'orange';
|
||||
case 'image':
|
||||
return 'purple';
|
||||
case 'creative':
|
||||
return 'magenta';
|
||||
case 'activity_outline':
|
||||
return 'green';
|
||||
default:
|
||||
return 'blue';
|
||||
}
|
||||
};
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
@@ -107,6 +129,13 @@
|
||||
align: 'center',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'chatType',
|
||||
key: 'chatType',
|
||||
align: 'center',
|
||||
width: 110
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'conversationId',
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<h1>活动策划大纲生成</h1>
|
||||
<p>填写活动主题、受众、时间和预算,AI 给出一份结构完整的策划大纲,可直接改、复制或导出。</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a-button ghost @click="goHistory">历史记录</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[24, 24]">
|
||||
@@ -169,6 +172,7 @@
|
||||
import zh_Hans from 'bytemd/locales/zh_Hans.json';
|
||||
import { getProcessor } from 'bytemd';
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { messageLoading } from 'ele-admin-pro';
|
||||
import { saveAs } from 'file-saver';
|
||||
@@ -183,6 +187,14 @@
|
||||
import { FAILED_TEXT } from '@/utils/ai-result';
|
||||
|
||||
type ExampleType = 'volunteer' | 'salon' | 'sports';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 活动策划记录统一在助手页查看,这里只带类型跳过去 */
|
||||
const goHistory = () => {
|
||||
push({ path: '/ai', query: { type: 'activity_outline' } });
|
||||
};
|
||||
|
||||
/** 等「流结束」的挂起点:只由 __END__ 帧或请求收尾来 resolve */
|
||||
type StreamResolver = {
|
||||
resolve: () => void;
|
||||
@@ -665,6 +677,18 @@
|
||||
color: rgba(255, 238, 238, 0.9);
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.hero-actions :deep(.ant-btn) {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.quick-actions-panel {
|
||||
margin-bottom: 14px;
|
||||
padding: 16px;
|
||||
@@ -805,5 +829,9 @@
|
||||
.hero-panel h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
支持文本指令与多类型附件联合驱动工作流,自动返回视频、脚本 Markdown 或图像/排版结果。
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a-button ghost @click="goHistory">历史记录</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[24, 24]">
|
||||
@@ -187,6 +190,7 @@
|
||||
import highlight from '@bytemd/plugin-highlight-ssr';
|
||||
import 'highlight.js/styles/default.css';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import ByteMdViewer from '@/components/ByteMdViewer/index.vue';
|
||||
import { runCreativeAssistantStream } from '@/utils/ai-stream';
|
||||
@@ -200,6 +204,13 @@
|
||||
|
||||
type ExampleType = 'video' | 'script' | 'image';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 创意助手记录统一在助手页查看,这里只带类型跳过去 */
|
||||
const goHistory = () => {
|
||||
push({ path: '/ai', query: { type: 'creative' } });
|
||||
};
|
||||
|
||||
const plugins = ref([
|
||||
gfm({
|
||||
locale: zh_HansGfm
|
||||
@@ -497,6 +508,18 @@
|
||||
color: rgba(255, 239, 241, 0.88);
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.hero-actions :deep(.ant-btn) {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.panel-card {
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 14px 34px rgba(87, 54, 30, 0.08);
|
||||
@@ -824,6 +847,10 @@
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.video-grid,
|
||||
.photo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a-button type="primary" ghost @click="goChatPage">进入问答助手</a-button>
|
||||
<a-button type="primary" ghost @click="goHistory">历史记录</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -844,6 +845,11 @@
|
||||
push('/ai');
|
||||
};
|
||||
|
||||
/** 写作记录统一在助手页查看,这里只带类型跳过去 */
|
||||
const goHistory = () => {
|
||||
push({ path: '/ai', query: { type: 'writing' } });
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearStreamResolver();
|
||||
});
|
||||
|
||||
+533
-34
@@ -22,6 +22,20 @@
|
||||
</button>
|
||||
|
||||
<div class="history-block">
|
||||
<div class="history-heading">历史记录</div>
|
||||
<div class="history-hint">仅展示最近 30 条</div>
|
||||
<div class="history-filter">
|
||||
<button
|
||||
v-for="item in historyTypeFilters"
|
||||
:key="item.value"
|
||||
class="history-filter-btn"
|
||||
type="button"
|
||||
:class="{ 'is-active': historyTypeFilter === item.value }"
|
||||
@click="historyTypeFilter = item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="historySections.length">
|
||||
<template v-for="section in historySections" :key="section.label">
|
||||
<div class="history-label">{{ section.label }}</div>
|
||||
@@ -37,7 +51,11 @@
|
||||
:disabled="loading"
|
||||
@click="selectChat(item)"
|
||||
>
|
||||
{{ getHistoryTitle(item) }}
|
||||
<span
|
||||
class="history-type-badge"
|
||||
:class="`is-${getHistoryType(item)}`"
|
||||
>{{ getHistoryTypeLabel(item) }}</span>
|
||||
<span class="history-title-text">{{ getHistoryTitle(item) }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="history-delete-btn"
|
||||
@@ -73,7 +91,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<div v-else class="history-empty">暂无历史对话</div>
|
||||
<div v-else class="history-empty">暂无历史记录</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
@@ -97,6 +115,24 @@
|
||||
<main class="chat-main">
|
||||
<div class="chat-main-header">{{ chatHeaderText }}</div>
|
||||
|
||||
<!--
|
||||
选中写作 / 图片 / 创意 / 活动策划的历史记录后,消息区顶部给一条说明:
|
||||
写作与图片可回填参数「再来一次」(不自动发送),另外两类只读。
|
||||
-->
|
||||
<div
|
||||
v-if="activeHistoryItem && activeHistoryItemType !== 'chat'"
|
||||
class="history-banner"
|
||||
>
|
||||
<span class="history-banner-text">{{ historyBannerText }}</span>
|
||||
<button
|
||||
v-if="canReplayActiveHistory"
|
||||
class="history-replay-btn"
|
||||
type="button"
|
||||
:disabled="loading || !activeHistoryItem?.params"
|
||||
@click="replayActiveHistory"
|
||||
>按原参数再来一次</button>
|
||||
</div>
|
||||
|
||||
<div class="chat-body" ref="chatBodyRef">
|
||||
<template v-if="messages.length">
|
||||
<div
|
||||
@@ -127,11 +163,13 @@
|
||||
{{ getAssistantDisplayContent(item.content) }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="getAssistantImageUrl(item.content)"
|
||||
v-else-if="getAssistantImageUrls(item.content).length"
|
||||
class="assistant-image-wrap"
|
||||
>
|
||||
<a-image
|
||||
:src="getAssistantImageUrl(item.content)"
|
||||
v-for="(imageUrl, imageIndex) in getAssistantImageUrls(item.content)"
|
||||
:key="`${item.id}-image-${imageIndex}`"
|
||||
:src="imageUrl"
|
||||
class="assistant-image"
|
||||
/>
|
||||
</div>
|
||||
@@ -348,7 +386,7 @@
|
||||
type="button"
|
||||
:class="{ 'is-active': activeChatMode === item.value }"
|
||||
:disabled="loading"
|
||||
@click="activeChatMode = item.value"
|
||||
@click="switchMode(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
@@ -375,7 +413,7 @@
|
||||
import "bytemd/dist/index.min.css";
|
||||
import dayjs from "dayjs";
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import "github-markdown-css/github-markdown-light.css";
|
||||
import gfm from "@bytemd/plugin-gfm";
|
||||
import zh_HansGfm from "@bytemd/plugin-gfm/locales/zh_Hans.json";
|
||||
@@ -402,9 +440,12 @@ import { logout } from "@/utils/page-tab-util";
|
||||
import { copyText } from "@/utils/common";
|
||||
import { resolveDisplayText, isFailedPayload, pickDisplayable, FAILED_TEXT } from "@/utils/ai-result";
|
||||
import { getToken } from "@/utils/token-util";
|
||||
import { AI_CHAT_TYPE_LABELS, AI_CHAT_TYPE_OPTIONS, resolveAiChatType } from "@/utils/ai-chat-type";
|
||||
import type { AiChatType } from "@/utils/ai-chat-type";
|
||||
|
||||
type ChatRole = "user" | "assistant";
|
||||
type ChatMode = "chat" | "writing" | "image";
|
||||
type HistoryType = AiChatType;
|
||||
|
||||
interface ChatMessageItem {
|
||||
id: string;
|
||||
@@ -413,6 +454,15 @@ interface ChatMessageItem {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
/** 每种模式一份独立工作区:消息、当前记录、输入草稿互不干扰 */
|
||||
interface ChatWorkspace {
|
||||
messages: ChatMessageItem[];
|
||||
activeChatId?: number;
|
||||
activeConversationId?: string;
|
||||
/** 输入框草稿:切到别的模式再切回来要还在 */
|
||||
draft: string;
|
||||
}
|
||||
|
||||
interface HistorySection {
|
||||
label: string;
|
||||
items: AiChatList[];
|
||||
@@ -434,8 +484,50 @@ const THINK_OPEN_TAG = "<think>";
|
||||
const THINK_CLOSE_TAG = "</think>";
|
||||
const loading = ref(false);
|
||||
const stopping = ref(false);
|
||||
const content = ref("");
|
||||
const activeChatMode = ref<ChatMode>("chat");
|
||||
// 三种模式各自一份工作区:切换模式只换视图,不丢消息、进行中的会话与输入草稿
|
||||
const createWorkspace = (): ChatWorkspace => ({
|
||||
messages: [],
|
||||
draft: ""
|
||||
});
|
||||
const workspaces = reactive<Record<ChatMode, ChatWorkspace>>({
|
||||
chat: createWorkspace(),
|
||||
writing: createWorkspace(),
|
||||
image: createWorkspace()
|
||||
});
|
||||
const activeWorkspace = computed(() => workspaces[activeChatMode.value]);
|
||||
// 创意 / 活动策划没有对应模式,选中时进只读预览,不往工作区里塞
|
||||
const previewHistory = ref<AiChatList | null>(null);
|
||||
const previewMessages = ref<ChatMessageItem[]>([]);
|
||||
const messages = computed<ChatMessageItem[]>({
|
||||
get: () =>
|
||||
previewHistory.value ? previewMessages.value : activeWorkspace.value.messages,
|
||||
set: (list) => {
|
||||
if (previewHistory.value) {
|
||||
previewMessages.value = list;
|
||||
return;
|
||||
}
|
||||
activeWorkspace.value.messages = list;
|
||||
}
|
||||
});
|
||||
const content = computed<string>({
|
||||
get: () => activeWorkspace.value.draft,
|
||||
set: (value) => {
|
||||
activeWorkspace.value.draft = value;
|
||||
}
|
||||
});
|
||||
const activeChatId = computed<number | undefined>({
|
||||
get: () => activeWorkspace.value.activeChatId,
|
||||
set: (value) => {
|
||||
activeWorkspace.value.activeChatId = value;
|
||||
}
|
||||
});
|
||||
const activeConversationId = computed<string | undefined>({
|
||||
get: () => activeWorkspace.value.activeConversationId,
|
||||
set: (value) => {
|
||||
activeWorkspace.value.activeConversationId = value;
|
||||
}
|
||||
});
|
||||
// 写作模式的显式选择:默认 auto = 不下发这两个字段的值,交给上游 Dify 识别
|
||||
const writingDocType = ref(WRITING_DOC_TYPE_FREE);
|
||||
const writingWordCount = ref("auto");
|
||||
@@ -452,11 +544,14 @@ const writingParams = reactive({
|
||||
});
|
||||
const wsConnected = ref(false);
|
||||
const deletingChatId = ref<number>();
|
||||
const messages = ref<ChatMessageItem[]>([]);
|
||||
const historyList = ref<AiChatList[]>([]);
|
||||
// 历史面板的类型筛选:纯前端过滤(列表一次拉全量)
|
||||
const historyTypeFilter = ref<HistoryType | "all">("all");
|
||||
const historyTypeFilters: { label: string; value: HistoryType | "all" }[] = [
|
||||
{ label: "全部", value: "all" },
|
||||
...AI_CHAT_TYPE_OPTIONS
|
||||
];
|
||||
const chatBodyRef = ref<HTMLElement | null>(null);
|
||||
const activeChatId = ref<number>();
|
||||
const activeConversationId = ref<string>();
|
||||
const streamingMessageId = ref<string>();
|
||||
const currentTaskId = ref<string>();
|
||||
const currentAbortController = ref<AbortController | null>(null);
|
||||
@@ -464,6 +559,7 @@ const webSocketRef = ref<WebSocket | null>(null);
|
||||
const wsConnectPromiseRef = ref<Promise<void> | null>(null);
|
||||
const userStore = useUserStore();
|
||||
const { push } = useRouter();
|
||||
const route = useRoute();
|
||||
const loginUser = computed(() => userStore.info ?? {});
|
||||
console.log('loginUser', loginUser)
|
||||
const userName = computed(
|
||||
@@ -499,6 +595,20 @@ const chatModeOptions: { label: string; value: ChatMode }[] = [
|
||||
{ label: "图片", value: "image" }
|
||||
];
|
||||
|
||||
/** 切换模式:各模式的工作区各自保留,切回来时消息、记录与草稿原样恢复 */
|
||||
const switchMode = (mode: ChatMode) => {
|
||||
if (mode === activeChatMode.value) {
|
||||
return;
|
||||
}
|
||||
if (loading.value) {
|
||||
message.warning("当前正在生成回复,请稍后再切换模式");
|
||||
return;
|
||||
}
|
||||
previewHistory.value = null;
|
||||
activeChatMode.value = mode;
|
||||
scrollToBottom();
|
||||
};
|
||||
|
||||
// 图片比例选择器已撤掉:上游创意助手开始节点没有比例变量(实测只有 user_content /
|
||||
// user_files),前端只能把它拼进 user_content —— 那是替上游写 prompt。
|
||||
// 要恢复这个功能,先在上游开始节点加变量,再按变量契约接上来。
|
||||
@@ -550,7 +660,27 @@ const chatHeaderText = computed(() => {
|
||||
return "AI助手待命中";
|
||||
});
|
||||
|
||||
/** 记录类型:缺失或未知一律按聊天处理(旧数据没有 chatType) */
|
||||
const getHistoryType = (item: AiChatList): HistoryType =>
|
||||
resolveAiChatType(item.chatType);
|
||||
|
||||
const getHistoryTypeLabel = (item: AiChatList) =>
|
||||
AI_CHAT_TYPE_LABELS[getHistoryType(item)];
|
||||
|
||||
// 纯前端筛选:列表一次拉全量,切类型不再打接口
|
||||
const filteredHistoryList = computed(() => {
|
||||
if (historyTypeFilter.value === "all") {
|
||||
return historyList.value;
|
||||
}
|
||||
return historyList.value.filter(
|
||||
(item) => getHistoryType(item) === historyTypeFilter.value
|
||||
);
|
||||
});
|
||||
|
||||
const activeHistoryKey = computed(() => {
|
||||
if (previewHistory.value) {
|
||||
return getHistoryKey(previewHistory.value);
|
||||
}
|
||||
if (activeChatId.value) {
|
||||
return `chat-${activeChatId.value}`;
|
||||
}
|
||||
@@ -560,9 +690,53 @@ const activeHistoryKey = computed(() => {
|
||||
return "";
|
||||
});
|
||||
|
||||
const activeHistoryItem = computed<AiChatList | null>(() => {
|
||||
if (previewHistory.value) {
|
||||
return previewHistory.value;
|
||||
}
|
||||
if (!activeChatId.value) {
|
||||
return null;
|
||||
}
|
||||
return historyList.value.find((item) => item.id === activeChatId.value) ?? null;
|
||||
});
|
||||
|
||||
const activeHistoryItemType = computed<HistoryType | "">(() =>
|
||||
activeHistoryItem.value ? getHistoryType(activeHistoryItem.value) : ""
|
||||
);
|
||||
|
||||
/** 「再来一次」只对写作 / 图片记录开放;其余类型只读 */
|
||||
const canReplayActiveHistory = computed(
|
||||
() =>
|
||||
activeHistoryItemType.value === "writing" ||
|
||||
activeHistoryItemType.value === "image"
|
||||
);
|
||||
|
||||
const historyBannerText = computed(() => {
|
||||
const item = activeHistoryItem.value;
|
||||
if (!item) {
|
||||
return "";
|
||||
}
|
||||
switch (getHistoryType(item)) {
|
||||
case "writing":
|
||||
return item.params
|
||||
? "这是写作记录,可回填文体与写作参数后重新生成(不会自动发送)。"
|
||||
: "这是写作记录,未保存原参数,无法回填。";
|
||||
case "image":
|
||||
return item.params
|
||||
? "这是图片记录,可回填提示词后重新生成(不会自动发送)。"
|
||||
: "这是图片记录,未保存原提示词,无法回填。";
|
||||
case "creative":
|
||||
return "创意助手记录为只读,请到「AI创意助手」页面查看。";
|
||||
case "activity_outline":
|
||||
return "活动策划记录为只读,请到「活动策划大纲生成」页面查看。";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const historySections = computed<HistorySection[]>(() => {
|
||||
const groupMap = new Map<string, AiChatList[]>();
|
||||
[...historyList.value]
|
||||
[...filteredHistoryList.value]
|
||||
.sort((a, b) => {
|
||||
const left = dayjs(b.updateTime || b.createTime || 0).valueOf();
|
||||
const right = dayjs(a.updateTime || a.createTime || 0).valueOf();
|
||||
@@ -651,19 +825,36 @@ const getAssistantDisplayContent = (value: string) =>
|
||||
const hasAssistantDisplayContent = (value: string) =>
|
||||
getAssistantDisplayContent(value).trim().length > 0;
|
||||
|
||||
const getAssistantImageUrl = (value: string) => {
|
||||
/**
|
||||
* 从回复里取出图片地址。
|
||||
*
|
||||
* 生图结果可能是:
|
||||
* ① 一条纯 URL / data:image(旧数据);
|
||||
* ② 后端把多张图拼成的多个 ``(见契约 5:多张图用空行分隔)。
|
||||
* 只有整段内容**全是** markdown 图片时才按图片渲染,正文里夹带插图仍走 markdown 分支。
|
||||
*/
|
||||
const getAssistantImageUrls = (value: string) => {
|
||||
const content = getAssistantDisplayContent(value).trim();
|
||||
if (!content) {
|
||||
return "";
|
||||
return [];
|
||||
}
|
||||
if (/^(https?:\/\/|data:image\/)/i.test(content) && /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(content.split("\n")[0]) || /^data:image\//i.test(content)) {
|
||||
return content;
|
||||
const firstLine = content.split("\n")[0].trim();
|
||||
if (
|
||||
/^data:image\//i.test(firstLine) ||
|
||||
(/^https?:\/\//i.test(firstLine) &&
|
||||
/\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(firstLine))
|
||||
) {
|
||||
return [firstLine];
|
||||
}
|
||||
const markdownImageMatch = content.match(/^!\[[^\]]*]\((.+?)\)$/);
|
||||
if (markdownImageMatch?.[1]) {
|
||||
return markdownImageMatch[1];
|
||||
const urls: string[] = [];
|
||||
const rest = content.replace(/!\[[^\]]*]\(([^)\s]+)\)/g, (_match, url: string) => {
|
||||
const text = String(url || "").trim();
|
||||
if (text) {
|
||||
urls.push(text);
|
||||
}
|
||||
return "";
|
||||
});
|
||||
return urls.length && !rest.trim() ? urls : [];
|
||||
};
|
||||
|
||||
const shouldShowAssistantThinking = (item: ChatMessageItem) => {
|
||||
@@ -695,10 +886,14 @@ const getHistoryTitle = (item: AiChatList) => {
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
// 「会话 xxx」只保留给聊天:生成记录没有会话,回退成「未命名记录」
|
||||
if (getHistoryType(item) === "chat") {
|
||||
if (item.conversationId) {
|
||||
return `会话 ${item.conversationId.slice(0, 8)}`;
|
||||
}
|
||||
return "未命名对话";
|
||||
}
|
||||
return "未命名记录";
|
||||
};
|
||||
|
||||
const getHistoryDateLabel = (value?: string) => {
|
||||
@@ -845,10 +1040,18 @@ const appendAssistantChunk = (answer: string, conversationId?: string) => {
|
||||
scrollToBottom();
|
||||
};
|
||||
|
||||
const finishAssistantMessage = async (conversationId?: string) => {
|
||||
const finishAssistantMessage = async (
|
||||
conversationId?: string,
|
||||
chatId?: number | string
|
||||
) => {
|
||||
if (conversationId) {
|
||||
activeConversationId.value = conversationId;
|
||||
}
|
||||
// 聊天记录也改为成功后才建行:__END__ 带 chatId 时直接认领
|
||||
const generatedChatId = Number(chatId);
|
||||
if (Number.isFinite(generatedChatId) && generatedChatId > 0) {
|
||||
activeChatId.value = generatedChatId;
|
||||
}
|
||||
const target = messages.value.find(
|
||||
(item) => item.id === streamingMessageId.value
|
||||
);
|
||||
@@ -911,7 +1114,8 @@ const handleSocketMessage = async (raw: string) => {
|
||||
currentTaskId.value = data.taskId;
|
||||
}
|
||||
if (data?.answer === "__END__") {
|
||||
await finishAssistantMessage(data.conversationId);
|
||||
// 契约:__END__ 帧可能带 chatId(记录成功写入时才有)
|
||||
await finishAssistantMessage(data.conversationId, data.chatId);
|
||||
disconnectWebSocket();
|
||||
return;
|
||||
}
|
||||
@@ -1004,14 +1208,25 @@ const loadHistoryList = async (autoSelect = true) => {
|
||||
}
|
||||
const response = await listAiChatList({ userId });
|
||||
historyList.value = response || [];
|
||||
syncActiveChat();
|
||||
// 预览的记录被删掉后,别让消息区停在旧内容上
|
||||
if (
|
||||
autoSelect &&
|
||||
!activeChatId.value &&
|
||||
!activeConversationId.value &&
|
||||
historyList.value.length
|
||||
previewHistory.value &&
|
||||
!historyList.value.some((item) => item.id === previewHistory.value?.id)
|
||||
) {
|
||||
await selectChat(historyList.value[0], true);
|
||||
previewHistory.value = null;
|
||||
previewMessages.value = [];
|
||||
}
|
||||
syncActiveChat();
|
||||
if (autoSelect && !activeChatId.value && !activeConversationId.value) {
|
||||
// 默认仍打开最近的聊天;带类型筛选时打开该类型的第一条
|
||||
const autoType =
|
||||
historyTypeFilter.value === "all" ? "chat" : historyTypeFilter.value;
|
||||
const target = historyList.value.find(
|
||||
(item) => getHistoryType(item) === autoType
|
||||
);
|
||||
if (target) {
|
||||
await selectChat(target, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1028,9 +1243,20 @@ const loadChatHistory = async (item: AiChatList) => {
|
||||
|
||||
const selectChat = async (item: AiChatList, silent = false) => {
|
||||
if (loading.value && !silent) {
|
||||
message.warning("当前正在生成回复,请稍后再切换会话");
|
||||
message.warning("当前正在生成回复,请稍后再切换历史记录");
|
||||
return;
|
||||
}
|
||||
const type = getHistoryType(item);
|
||||
// 创意 / 活动策划没有对应模式:进只读预览,不覆盖任何模式的工作区
|
||||
if (type === "creative" || type === "activity_outline") {
|
||||
previewHistory.value = item;
|
||||
await loadChatHistory(item);
|
||||
return;
|
||||
}
|
||||
previewHistory.value = null;
|
||||
// 打开记录时切到它所属的模式,历史内容落在各自的工作区里
|
||||
activeChatMode.value =
|
||||
type === "writing" ? "writing" : type === "image" ? "image" : "chat";
|
||||
activeChatId.value = item.id;
|
||||
activeConversationId.value = item.conversationId;
|
||||
await loadChatHistory(item);
|
||||
@@ -1042,6 +1268,8 @@ const startNewChat = () => {
|
||||
return;
|
||||
}
|
||||
disconnectWebSocket();
|
||||
previewHistory.value = null;
|
||||
previewMessages.value = [];
|
||||
activeChatId.value = undefined;
|
||||
activeConversationId.value = undefined;
|
||||
streamingMessageId.value = undefined;
|
||||
@@ -1049,6 +1277,110 @@ const startNewChat = () => {
|
||||
content.value = "";
|
||||
};
|
||||
|
||||
/** 从入参快照里按候选键取字符串(后端可能存 camelCase 或 snake_case) */
|
||||
const readHistoryParam = (params: Record<string, any>, keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
const value = params[key];
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
/** 解析 `params` 快照;不是合法 JSON 对象时返回 null(调用方忽略) */
|
||||
const parseHistoryParams = (value?: string) => {
|
||||
const text = String(value || "").trim();
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||
return parsed as Record<string, any>;
|
||||
}
|
||||
} catch {
|
||||
// 解析失败按「没有参数」处理,不影响其它记录
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** 「再来一次」:把写作参数与正文回填到写作面板,不自动发送 */
|
||||
const applyWritingHistoryParams = (params: Record<string, any>) => {
|
||||
const docType = readHistoryParam(params, ["docType", "doc_type"]);
|
||||
writingDocType.value = WRITING_DOC_TYPE_OPTIONS.some(
|
||||
(item) => item.value === docType
|
||||
)
|
||||
? docType
|
||||
: WRITING_DOC_TYPE_FREE;
|
||||
const eventType = readHistoryParam(params, ["eventType", "event_type"]);
|
||||
writingParams.eventType = WRITING_EVENT_TYPE_OPTIONS.includes(eventType)
|
||||
? eventType
|
||||
: "";
|
||||
writingParams.theme = readHistoryParam(params, ["theme"]);
|
||||
writingParams.time = readHistoryParam(params, ["time"]);
|
||||
writingParams.location = readHistoryParam(params, ["location"]);
|
||||
writingParams.participants = readHistoryParam(params, ["participants"]);
|
||||
const wordCount = readHistoryParam(params, [
|
||||
"number",
|
||||
"wordCount",
|
||||
"word_count"
|
||||
]);
|
||||
writingWordCount.value = WRITING_WORD_COUNT_OPTIONS.some(
|
||||
(item) => item.value === wordCount
|
||||
)
|
||||
? wordCount
|
||||
: "auto";
|
||||
// 模板模式的正文是 highlights,自由创作是 userContent
|
||||
content.value = readHistoryParam(params, [
|
||||
"highlights",
|
||||
"userContent",
|
||||
"user_content"
|
||||
]);
|
||||
if (writingTemplateMode.value && writingParamCount.value > 0) {
|
||||
writingParamsOpen.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
/** 「再来一次」:图片记录只回填提示词 */
|
||||
const applyImageHistoryParams = (params: Record<string, any>) => {
|
||||
content.value = readHistoryParam(params, [
|
||||
"user_content",
|
||||
"userContent",
|
||||
"content",
|
||||
"query"
|
||||
]);
|
||||
};
|
||||
|
||||
/** 回填选中记录的原参数,交给用户确认后再发送 */
|
||||
const replayActiveHistory = () => {
|
||||
const item = activeHistoryItem.value;
|
||||
if (!item || !canReplayActiveHistory.value) {
|
||||
return;
|
||||
}
|
||||
if (loading.value) {
|
||||
message.warning("当前正在生成,请稍后再回填参数");
|
||||
return;
|
||||
}
|
||||
const params = parseHistoryParams(item.params);
|
||||
if (!params) {
|
||||
message.warning("这条记录没有可回填的参数");
|
||||
return;
|
||||
}
|
||||
previewHistory.value = null;
|
||||
if (getHistoryType(item) === "writing") {
|
||||
activeChatMode.value = "writing";
|
||||
applyWritingHistoryParams(params);
|
||||
} else {
|
||||
activeChatMode.value = "image";
|
||||
applyImageHistoryParams(params);
|
||||
}
|
||||
message.success("已回填原参数,确认后点击发送");
|
||||
};
|
||||
|
||||
// 原本这里有一个 stringifyUnknown(JSON.stringify 整包兜底),已移除:
|
||||
// 它会把 conversationId / taskId / workflowRunId 等内部标识符渲染给用户。
|
||||
// 现在统一走 @/utils/ai-result 的 resolveDisplayText(见 issue 10)。
|
||||
@@ -1304,6 +1636,19 @@ const buildImagePayload = (query: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成成功后认领后端写入的记录:末帧带回 chatId 就设为当前高亮项并刷新列表;
|
||||
* 没带回(失败态 / 后端未回)就只刷新列表,不猜 id。
|
||||
*/
|
||||
const claimGeneratedChatId = async (result: unknown) => {
|
||||
const chatId = Number((result as any)?.chatId);
|
||||
if (Number.isFinite(chatId) && chatId > 0) {
|
||||
activeChatId.value = chatId;
|
||||
activeConversationId.value = undefined;
|
||||
}
|
||||
await loadHistoryList(false);
|
||||
};
|
||||
|
||||
const sendModeMessage = async (
|
||||
query: string,
|
||||
assistantId: string,
|
||||
@@ -1323,6 +1668,7 @@ const sendModeMessage = async (
|
||||
currentTaskId.value = (result as any).taskId;
|
||||
}
|
||||
setResult(formatWritingResult(result));
|
||||
await claimGeneratedChatId(result);
|
||||
return;
|
||||
}
|
||||
if (activeChatMode.value === "image") {
|
||||
@@ -1332,16 +1678,17 @@ const sendModeMessage = async (
|
||||
currentTaskId.value = (result as any).taskId;
|
||||
}
|
||||
setResult(formatCreativeResult(result));
|
||||
await claimGeneratedChatId(result);
|
||||
}
|
||||
};
|
||||
|
||||
const removeChat = async (item: AiChatList) => {
|
||||
if (loading.value) {
|
||||
message.warning("当前正在生成回复,请稍后再删除会话");
|
||||
message.warning("当前正在生成回复,请稍后再删除历史记录");
|
||||
return;
|
||||
}
|
||||
if (!item.id) {
|
||||
message.error("当前会话缺少标识,无法删除");
|
||||
message.error("当前记录缺少标识,无法删除");
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`确定删除“${getHistoryTitle(item)}”吗?`)) {
|
||||
@@ -1349,17 +1696,23 @@ const removeChat = async (item: AiChatList) => {
|
||||
}
|
||||
deletingChatId.value = item.id;
|
||||
try {
|
||||
const isPreview = previewHistory.value?.id === item.id;
|
||||
const isActive =
|
||||
activeChatId.value === item.id ||
|
||||
activeConversationId.value === item.conversationId;
|
||||
(!!item.conversationId &&
|
||||
activeConversationId.value === item.conversationId);
|
||||
await removeAiChatList(item.id);
|
||||
if (isPreview) {
|
||||
previewHistory.value = null;
|
||||
previewMessages.value = [];
|
||||
}
|
||||
await loadHistoryList(false);
|
||||
if (isActive) {
|
||||
const nextChat = historyList.value[0];
|
||||
const nextChat = filteredHistoryList.value.find(
|
||||
(row) => row.id !== item.id
|
||||
);
|
||||
if (nextChat) {
|
||||
activeChatId.value = nextChat.id;
|
||||
activeConversationId.value = nextChat.conversationId;
|
||||
await loadChatHistory(nextChat);
|
||||
await selectChat(nextChat, true);
|
||||
} else {
|
||||
startNewChat();
|
||||
}
|
||||
@@ -1426,6 +1779,14 @@ const onSend = async () => {
|
||||
message.error("未获取到当前登录用户");
|
||||
return;
|
||||
}
|
||||
// 从只读预览切回当前模式的工作区再追加消息,别把新消息写进预览里
|
||||
previewHistory.value = null;
|
||||
previewMessages.value = [];
|
||||
// 写作 / 图片每次生成都是一条独立记录,先清掉上一条的高亮
|
||||
if (activeChatMode.value !== "chat") {
|
||||
activeChatId.value = undefined;
|
||||
activeConversationId.value = undefined;
|
||||
}
|
||||
const query = content.value.trim();
|
||||
// 只填了写作参数、没写正文时,气泡上仍要有一句话,否则是一条空消息
|
||||
const displayQuery =
|
||||
@@ -1505,6 +1866,20 @@ const onSend = async () => {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 独立页跳过来时带 ?type=<chat_type>:预置历史筛选,写作 / 图片顺便切到对应模式
|
||||
const queryType = String(route.query.type || "").trim();
|
||||
if (
|
||||
queryType === "chat" ||
|
||||
queryType === "writing" ||
|
||||
queryType === "image" ||
|
||||
queryType === "creative" ||
|
||||
queryType === "activity_outline"
|
||||
) {
|
||||
historyTypeFilter.value = queryType;
|
||||
if (queryType === "writing" || queryType === "image") {
|
||||
activeChatMode.value = queryType;
|
||||
}
|
||||
}
|
||||
if (!loginUser.value.userId) {
|
||||
await userStore.fetchUserInfo();
|
||||
}
|
||||
@@ -1689,6 +2064,41 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.history-heading {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.history-hint {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #8a94a6;
|
||||
}
|
||||
|
||||
.history-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.history-filter-btn {
|
||||
border: 1px solid rgba(21, 144, 255, 0.24);
|
||||
border-radius: 999px;
|
||||
background: #f5fbff;
|
||||
color: #1590ff;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 1px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.history-filter-btn.is-active {
|
||||
border-color: #1590ff;
|
||||
background: #1590ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.history-label {
|
||||
font-size: 12px;
|
||||
color: #5f7f9f;
|
||||
@@ -1706,6 +2116,9 @@ export default {
|
||||
.history-item-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -1713,11 +2126,50 @@ export default {
|
||||
font-size: 14px;
|
||||
padding: 8px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.history-title-text {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 类型徽标:颜色与历史筛选、后台表格保持一致 */
|
||||
.history-type-badge {
|
||||
flex: none;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.history-type-badge.is-chat {
|
||||
background: #e8f4ff;
|
||||
color: #1590ff;
|
||||
}
|
||||
|
||||
.history-type-badge.is-writing {
|
||||
background: #fff7e6;
|
||||
color: #d46b08;
|
||||
}
|
||||
|
||||
.history-type-badge.is-image {
|
||||
background: #f9f0ff;
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.history-type-badge.is-creative {
|
||||
background: #fff0f6;
|
||||
color: #c41d7f;
|
||||
}
|
||||
|
||||
.history-type-badge.is-activity_outline {
|
||||
background: #f6ffed;
|
||||
color: #389e0d;
|
||||
}
|
||||
|
||||
.history-item-main:disabled,
|
||||
.history-delete-btn:disabled {
|
||||
opacity: 0.6;
|
||||
@@ -1839,6 +2291,46 @@ export default {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.history-banner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
width: 800px;
|
||||
margin: 0 auto 14px;
|
||||
padding: 8px 12px;
|
||||
border: 1px dashed rgba(21, 144, 255, 0.28);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.history-banner-text {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
color: #5f7f9f;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.history-replay-btn {
|
||||
flex: none;
|
||||
border: 1px solid #1590ff;
|
||||
border-radius: 999px;
|
||||
background: #1590ff;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 3px 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.history-replay-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chat-body {
|
||||
flex: 1 1 0;
|
||||
height: 0;
|
||||
@@ -1928,6 +2420,9 @@ export default {
|
||||
}
|
||||
|
||||
.assistant-image-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
@@ -2374,5 +2869,9 @@ export default {
|
||||
.chat-main {
|
||||
padding: 24px 24px 0;
|
||||
}
|
||||
|
||||
.history-banner {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user