add AIAgent think
This commit is contained in:
@@ -20,10 +20,16 @@ export interface AiCitation {
|
|||||||
generatedAt?: string;
|
generatedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AiThinking {
|
||||||
|
id: number;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AiChatMessage {
|
export interface AiChatMessage {
|
||||||
id: number;
|
id: number;
|
||||||
role: AiMessageRole;
|
role: AiMessageRole;
|
||||||
content: string;
|
content: string;
|
||||||
|
thinkingList: AiThinking[];
|
||||||
citationList: AiCitation[];
|
citationList: AiCitation[];
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
statusText?: string;
|
statusText?: string;
|
||||||
@@ -37,6 +43,14 @@ export interface UseAiChatOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const conversationExpiredText = "AI 会话不存在或已过期";
|
const conversationExpiredText = "AI 会话不存在或已过期";
|
||||||
|
const thinkOpenMarkers = ["<thinking>", "<think>"];
|
||||||
|
const thinkCloseMarkers = ["</thinking>", "</think>"];
|
||||||
|
|
||||||
|
interface AssistantResponseState {
|
||||||
|
rawText: string;
|
||||||
|
eventThinkingList: AiThinking[];
|
||||||
|
taggedThinkingList: AiThinking[];
|
||||||
|
}
|
||||||
|
|
||||||
/** 管理门店智能助手的会话、流式消息和取消操作。 */
|
/** 管理门店智能助手的会话、流式消息和取消操作。 */
|
||||||
export function useAiChat(options: UseAiChatOptions) {
|
export function useAiChat(options: UseAiChatOptions) {
|
||||||
@@ -50,6 +64,7 @@ export function useAiChat(options: UseAiChatOptions) {
|
|||||||
const canRetry = computed(() => !!lastError.value && !!lastQuestion.value && !isGenerating.value);
|
const canRetry = computed(() => !!lastError.value && !!lastQuestion.value && !isGenerating.value);
|
||||||
let activeController: AbortController | undefined;
|
let activeController: AbortController | undefined;
|
||||||
let messageSequence = 0;
|
let messageSequence = 0;
|
||||||
|
const responseStateMap = new Map<number, AssistantResponseState>();
|
||||||
|
|
||||||
async function sendMessage(message: string): Promise<void> {
|
async function sendMessage(message: string): Promise<void> {
|
||||||
const normalizedMessage = message.trim();
|
const normalizedMessage = message.trim();
|
||||||
@@ -61,6 +76,7 @@ export function useAiChat(options: UseAiChatOptions) {
|
|||||||
id: nextMessageId(),
|
id: nextMessageId(),
|
||||||
role: "user",
|
role: "user",
|
||||||
content: normalizedMessage,
|
content: normalizedMessage,
|
||||||
|
thinkingList: [],
|
||||||
citationList: [],
|
citationList: [],
|
||||||
createdAt: Date.now()
|
createdAt: Date.now()
|
||||||
};
|
};
|
||||||
@@ -68,6 +84,7 @@ export function useAiChat(options: UseAiChatOptions) {
|
|||||||
id: nextMessageId(),
|
id: nextMessageId(),
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "",
|
content: "",
|
||||||
|
thinkingList: [],
|
||||||
citationList: [],
|
citationList: [],
|
||||||
createdAt: Date.now()
|
createdAt: Date.now()
|
||||||
};
|
};
|
||||||
@@ -106,6 +123,7 @@ export function useAiChat(options: UseAiChatOptions) {
|
|||||||
}
|
}
|
||||||
isGenerating.value = false;
|
isGenerating.value = false;
|
||||||
activeToolStatus.value = "";
|
activeToolStatus.value = "";
|
||||||
|
responseStateMap.delete(assistantMessage.id);
|
||||||
await scrollToBottom();
|
await scrollToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,6 +144,7 @@ export function useAiChat(options: UseAiChatOptions) {
|
|||||||
activeController?.abort();
|
activeController?.abort();
|
||||||
activeController = undefined;
|
activeController = undefined;
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
|
responseStateMap.clear();
|
||||||
conversationId.value = "";
|
conversationId.value = "";
|
||||||
isGenerating.value = false;
|
isGenerating.value = false;
|
||||||
activeToolStatus.value = "";
|
activeToolStatus.value = "";
|
||||||
@@ -161,11 +180,20 @@ export function useAiChat(options: UseAiChatOptions) {
|
|||||||
async function handleStreamEvent(event: GaoAiStreamEvent, assistantId: number, hasDoneEvent: boolean): Promise<boolean> {
|
async function handleStreamEvent(event: GaoAiStreamEvent, assistantId: number, hasDoneEvent: boolean): Promise<boolean> {
|
||||||
const payload = parseSseData(event.data);
|
const payload = parseSseData(event.data);
|
||||||
switch (event.event) {
|
switch (event.event) {
|
||||||
|
case "think":
|
||||||
|
case "thinking":
|
||||||
|
appendAssistantThinking(assistantId, extractThinkingText(payload));
|
||||||
|
return hasDoneEvent;
|
||||||
|
case "agent_thought":
|
||||||
|
appendAssistantThinking(assistantId, extractThinkingText(payload), true);
|
||||||
|
return hasDoneEvent;
|
||||||
case "message":
|
case "message":
|
||||||
appendAssistantText(assistantId, extractText(payload));
|
appendAssistantResponse(assistantId, extractText(payload));
|
||||||
return hasDoneEvent;
|
return hasDoneEvent;
|
||||||
case "tool_status":
|
case "tool_status":
|
||||||
activeToolStatus.value = formatToolStatus(payload);
|
{
|
||||||
|
activeToolStatus.value = formatToolStatus(payload);
|
||||||
|
}
|
||||||
return hasDoneEvent;
|
return hasDoneEvent;
|
||||||
case "citation":
|
case "citation":
|
||||||
appendCitation(assistantId, normalizeCitation(payload));
|
appendCitation(assistantId, normalizeCitation(payload));
|
||||||
@@ -195,15 +223,58 @@ export function useAiChat(options: UseAiChatOptions) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendAssistantText(assistantId: number, text: string): void {
|
function appendAssistantResponse(assistantId: number, text: string): void {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const message = messages.value.find(item => item.id === assistantId);
|
const state = getAssistantResponseState(assistantId);
|
||||||
if (!message) {
|
state.rawText = mergeResponseText(state.rawText, text);
|
||||||
|
const parsedResponse = parseAssistantResponse(state.rawText);
|
||||||
|
state.taggedThinkingList = parsedResponse.thinkingList.map((content, index) => ({
|
||||||
|
id: state.taggedThinkingList[index]?.id || nextThinkingId(),
|
||||||
|
content
|
||||||
|
}));
|
||||||
|
updateMessage(assistantId, {
|
||||||
|
content: parsedResponse.answer,
|
||||||
|
thinkingList: [...state.eventThinkingList, ...state.taggedThinkingList],
|
||||||
|
statusText: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAssistantResponseState(assistantId: number): AssistantResponseState {
|
||||||
|
const currentState = responseStateMap.get(assistantId);
|
||||||
|
if (currentState) {
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
const nextState: AssistantResponseState = {
|
||||||
|
rawText: "",
|
||||||
|
eventThinkingList: [],
|
||||||
|
taggedThinkingList: []
|
||||||
|
};
|
||||||
|
responseStateMap.set(assistantId, nextState);
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendAssistantThinking(assistantId: number, text: string, createSegment = false): void {
|
||||||
|
if (!text) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateMessage(assistantId, { content: message.content + text, statusText: undefined });
|
const state = getAssistantResponseState(assistantId);
|
||||||
|
const thinkingList = [...state.eventThinkingList];
|
||||||
|
const lastThinking = thinkingList[thinkingList.length - 1];
|
||||||
|
if (createSegment || !lastThinking) {
|
||||||
|
thinkingList.push({ id: nextThinkingId(), content: text });
|
||||||
|
} else {
|
||||||
|
thinkingList[thinkingList.length - 1] = {
|
||||||
|
...lastThinking,
|
||||||
|
content: lastThinking.content + text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
state.eventThinkingList = thinkingList;
|
||||||
|
updateMessage(assistantId, {
|
||||||
|
thinkingList: [...state.eventThinkingList, ...state.taggedThinkingList],
|
||||||
|
statusText: undefined
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendCitation(assistantId: number, citation?: AiCitation): void {
|
function appendCitation(assistantId: number, citation?: AiCitation): void {
|
||||||
@@ -222,8 +293,10 @@ export function useAiChat(options: UseAiChatOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetAssistantMessage(id: number): void {
|
function resetAssistantMessage(id: number): void {
|
||||||
|
responseStateMap.delete(id);
|
||||||
updateMessage(id, {
|
updateMessage(id, {
|
||||||
content: "",
|
content: "",
|
||||||
|
thinkingList: [],
|
||||||
citationList: [],
|
citationList: [],
|
||||||
statusText: undefined,
|
statusText: undefined,
|
||||||
duration: undefined,
|
duration: undefined,
|
||||||
@@ -242,6 +315,7 @@ export function useAiChat(options: UseAiChatOptions) {
|
|||||||
|
|
||||||
function clearMessages(): void {
|
function clearMessages(): void {
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
|
responseStateMap.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextMessageId(): number {
|
function nextMessageId(): number {
|
||||||
@@ -249,6 +323,13 @@ export function useAiChat(options: UseAiChatOptions) {
|
|||||||
return messageSequence;
|
return messageSequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let thinkingSequence = 0;
|
||||||
|
|
||||||
|
function nextThinkingId(): number {
|
||||||
|
thinkingSequence += 1;
|
||||||
|
return thinkingSequence;
|
||||||
|
}
|
||||||
|
|
||||||
async function scrollToBottom(): Promise<void> {
|
async function scrollToBottom(): Promise<void> {
|
||||||
await options.scrollToBottom?.();
|
await options.scrollToBottom?.();
|
||||||
}
|
}
|
||||||
@@ -290,7 +371,91 @@ function extractText(payload: unknown): string {
|
|||||||
if (!isRecord(payload)) {
|
if (!isRecord(payload)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return getString(payload.answer) || getString(payload.text) || getString(payload.content) || "";
|
return getRawString(payload.answer) || getRawString(payload.text) || getRawString(payload.content) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeResponseText(currentText: string, nextText: string): string {
|
||||||
|
if (currentText && nextText.startsWith(currentText)) {
|
||||||
|
return nextText;
|
||||||
|
}
|
||||||
|
return currentText + nextText;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAssistantResponse(value: string): { answer: string; thinkingList: string[] } {
|
||||||
|
let answer = "";
|
||||||
|
let thinking = "";
|
||||||
|
let mode: "answer" | "thinking" = "answer";
|
||||||
|
let cursor = 0;
|
||||||
|
const thinkingList: string[] = [];
|
||||||
|
|
||||||
|
while (cursor < value.length) {
|
||||||
|
const marker = findMarker(value.slice(cursor), [...thinkOpenMarkers, ...thinkCloseMarkers]);
|
||||||
|
if (!marker) {
|
||||||
|
const rest = value.slice(cursor);
|
||||||
|
if (mode === "thinking") {
|
||||||
|
thinking += rest;
|
||||||
|
} else {
|
||||||
|
answer += rest;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = value.slice(cursor, cursor + marker.index);
|
||||||
|
if (mode === "thinking") {
|
||||||
|
thinking += content;
|
||||||
|
if (thinking) {
|
||||||
|
thinkingList.push(thinking);
|
||||||
|
}
|
||||||
|
thinking = "";
|
||||||
|
mode = "answer";
|
||||||
|
} else if (isThinkOpenMarker(marker.value)) {
|
||||||
|
answer += content;
|
||||||
|
mode = "thinking";
|
||||||
|
} else {
|
||||||
|
if (content) {
|
||||||
|
thinkingList.push(content);
|
||||||
|
}
|
||||||
|
mode = "answer";
|
||||||
|
}
|
||||||
|
cursor += marker.index + marker.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "thinking" && thinking) {
|
||||||
|
thinkingList.push(thinking);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { answer, thinkingList };
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMarker(value: string, markerList: string[]): { index: number; value: string } | undefined {
|
||||||
|
const lowerValue = value.toLowerCase();
|
||||||
|
return markerList.reduce<{ index: number; value: string } | undefined>((current, marker) => {
|
||||||
|
const index = lowerValue.indexOf(marker.toLowerCase());
|
||||||
|
if (index < 0 || (current && current.index <= index)) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
return { index, value: marker };
|
||||||
|
}, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isThinkOpenMarker(value: string): boolean {
|
||||||
|
return thinkOpenMarkers.some(marker => marker.toLowerCase() === value.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractThinkingText(payload: unknown): string {
|
||||||
|
if (typeof payload === "string") {
|
||||||
|
return payload === "[DONE]" ? "" : payload;
|
||||||
|
}
|
||||||
|
if (!isRecord(payload)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return getRawString(payload.thought)
|
||||||
|
|| getRawString(payload.thinking)
|
||||||
|
|| getRawString(payload.reasoning)
|
||||||
|
|| getRawString(payload.observation)
|
||||||
|
|| getRawString(payload.text)
|
||||||
|
|| getRawString(payload.content)
|
||||||
|
|| "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatToolStatus(payload: unknown): string {
|
function formatToolStatus(payload: unknown): string {
|
||||||
@@ -364,6 +529,10 @@ function getString(value: unknown): string | undefined {
|
|||||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRawString(value: unknown): string {
|
||||||
|
return typeof value === "string" ? value : "";
|
||||||
|
}
|
||||||
|
|
||||||
function getDisplayText(value: unknown): string | undefined {
|
function getDisplayText(value: unknown): string | undefined {
|
||||||
const text = getString(value);
|
const text = getString(value);
|
||||||
if (text) {
|
if (text) {
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
<span class="error-text" v-text="lastError" />
|
<span class="error-text" v-text="lastError" />
|
||||||
<t-button v-if="canRetry" variant="text" size="small" content="重试" @click="emit('retry')" />
|
<t-button v-if="canRetry" variant="text" size="small" content="重试" @click="emit('retry')" />
|
||||||
</div>
|
</div>
|
||||||
<t-form class="input-wrap glass-white" @submit.prevent="submitMessage">
|
<t-form class="input-wrap glass-white" @submit="submitMessage">
|
||||||
<t-input
|
<t-input
|
||||||
v-model="draft"
|
v-model="draft"
|
||||||
class="input"
|
class="input"
|
||||||
borderless
|
borderless
|
||||||
:disabled="isGenerating"
|
:disabled="isGenerating"
|
||||||
:maxlength="256"
|
:maxlength="256"
|
||||||
placeholder="问问小糕怎么个事(●'◡'●)…"
|
placeholder="问问小糕怎么个事…"
|
||||||
/>
|
/>
|
||||||
<t-button
|
<t-button
|
||||||
v-if="isGenerating"
|
v-if="isGenerating"
|
||||||
@@ -93,9 +93,9 @@ function submitMessage(): void {
|
|||||||
|
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
border: 1px solid var(--td-component-border);
|
border: 1px solid var(--td-component-border);
|
||||||
|
margin: .5rem 0 0 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: .25rem .25rem .25rem .75rem;
|
padding: .25rem .25rem .25rem .75rem;
|
||||||
margin-top: .5rem;
|
|
||||||
box-shadow: 0 .5rem 1.5rem rgba(15, 23, 42, .12);
|
box-shadow: 0 .5rem 1.5rem rgba(15, 23, 42, .12);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
|
|||||||
@@ -24,12 +24,22 @@
|
|||||||
class="message-row"
|
class="message-row"
|
||||||
:class="message.role"
|
:class="message.role"
|
||||||
>
|
>
|
||||||
<div class="avatar">
|
<div class="message-header">
|
||||||
<t-icon :name="message.role === 'assistant' ? 'robot-2' : 'user'" />
|
<div class="avatar">
|
||||||
|
<t-icon :name="message.role === 'assistant' ? 'robot-2' : 'user'" />
|
||||||
|
</div>
|
||||||
|
<div class="role-name" v-text="message.role === 'assistant' ? 'AI 小糕' : '店长'" />
|
||||||
</div>
|
</div>
|
||||||
<div class="message-main">
|
<div class="message-main">
|
||||||
<div class="role-name" v-text="message.role === 'assistant' ? 'AI 小糕' : '店长'" />
|
|
||||||
<div class="bubble" :class="message.role">
|
<div class="bubble" :class="message.role">
|
||||||
|
<template v-if="message.role === 'assistant'">
|
||||||
|
<ai-chat-thinking
|
||||||
|
v-for="thinking in message.thinkingList"
|
||||||
|
:key="thinking.id"
|
||||||
|
:content="thinking.content"
|
||||||
|
:is-generating="props.isGenerating && thinking.id === message.thinkingList[message.thinkingList.length - 1]?.id"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<div v-if="message.content && message.role === 'assistant'" class="content markdown-content">
|
<div v-if="message.content && message.role === 'assistant'" class="content markdown-content">
|
||||||
<markdown-view :content="message.content" />
|
<markdown-view :content="message.content" />
|
||||||
</div>
|
</div>
|
||||||
@@ -57,6 +67,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { MarkdownView } from "timi-web";
|
import { MarkdownView } from "timi-web";
|
||||||
import type { AiChatMessage } from "@/composables/useAiChat";
|
import type { AiChatMessage } from "@/composables/useAiChat";
|
||||||
|
import AiChatThinking from "@/pages/ai/components/AiChatThinking.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "AiChatMessageList"
|
name: "AiChatMessageList"
|
||||||
@@ -124,7 +135,7 @@ defineExpose({
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: .5rem 1rem calc(12rem + env(safe-area-inset-bottom, 0px));
|
padding: .5rem 1rem calc(6rem + env(safe-area-inset-bottom, 0px));
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -173,10 +184,29 @@ defineExpose({
|
|||||||
.message-row {
|
.message-row {
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
&.user {
|
&.user {
|
||||||
flex-direction: row-reverse;
|
.message-header {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-name {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
gap: .5rem;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@@ -196,7 +226,8 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-main {
|
.message-main {
|
||||||
max-width: calc(100% - 2.5rem);
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.user .message-main {
|
&.user .message-main {
|
||||||
@@ -205,18 +236,23 @@ defineExpose({
|
|||||||
|
|
||||||
.role-name {
|
.role-name {
|
||||||
color: var(--td-text-color-secondary);
|
color: var(--td-text-color-secondary);
|
||||||
margin: 0 .25rem .25rem;
|
margin: 0;
|
||||||
font-size: .6875rem;
|
font-size: .6875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bubble {
|
.bubble {
|
||||||
color: var(--td-text-color-primary);
|
color: var(--td-text-color-primary);
|
||||||
|
width: 100%;
|
||||||
padding: .75rem;
|
padding: .75rem;
|
||||||
border-radius: .25rem 1rem 1rem 1rem;
|
border-radius: .25rem 1rem 1rem 1rem;
|
||||||
background: var(--ai-bubble);
|
background: var(--ai-bubble);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
box-shadow: 0 .25rem 1rem rgba(15, 23, 42, .05);
|
box-shadow: 0 .25rem 1rem rgba(15, 23, 42, .05);
|
||||||
|
|
||||||
|
.thinking + .thinking {
|
||||||
|
margin-top: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
&.user {
|
&.user {
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 1rem .25rem 1rem 1rem;
|
border-radius: 1rem .25rem 1rem 1rem;
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<section class="thinking" aria-label="Agent think">
|
||||||
|
<div class="thinking-header">
|
||||||
|
<span>小糕正在思考...</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref="thinkingContent"
|
||||||
|
class="thinking-content"
|
||||||
|
role="log"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<span v-text="props.content" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
content: string;
|
||||||
|
isGenerating: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const thinkingContentRef = useTemplateRef<HTMLElement>("thinkingContent");
|
||||||
|
|
||||||
|
// ---------- 自动滚动 ----------
|
||||||
|
|
||||||
|
async function scrollToBottom(): Promise<void> {
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const element = thinkingContentRef.value;
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.scrollTop = element.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => props.content, () => props.isGenerating],
|
||||||
|
async ([content, isGenerating]) => {
|
||||||
|
if (content && isGenerating) {
|
||||||
|
await scrollToBottom();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, flush: "post" }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<style scoped lang="less">
|
||||||
|
.thinking {
|
||||||
|
gap: .5rem;
|
||||||
|
height: clamp(2rem, 24vh, 10rem);
|
||||||
|
display: flex;
|
||||||
|
padding: .625rem .75rem;
|
||||||
|
border: 1px solid var(--td-component-border);
|
||||||
|
border-radius: .75rem;
|
||||||
|
background: var(--ai-background);
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 2rem;
|
||||||
|
max-height: 10rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.thinking-header {
|
||||||
|
gap: .375rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
font-size: .75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-content {
|
||||||
|
flex: 1;
|
||||||
|
padding-right: .25rem;
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
font-size: .75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user