update aiChatQuestions.ts from api
This commit is contained in:
@@ -1,8 +1,141 @@
|
|||||||
/** 门店智能助手快捷问题。 */
|
import { ref } from "vue";
|
||||||
export const aiQuickQuestionList = [
|
import { SettingAPI } from "timi-web-api";
|
||||||
"今天门店经营怎么样?",
|
|
||||||
"本月销售额、订单数和应收款是多少?",
|
const AI_QUICK_QUESTION_CACHE_KEY = "gao-customer-h5:ai-agent-example";
|
||||||
"哪些产品库存需要关注?",
|
const AI_QUICK_QUESTION_CACHE_TTL = 24 * 60 * 60 * 1000;
|
||||||
"本月有哪些需要重点跟进的客户?",
|
const AI_QUICK_QUESTION_COUNT = 5;
|
||||||
"本月客户复购情况如何?"
|
|
||||||
] as const;
|
interface AiQuickQuestionCache {
|
||||||
|
cachedAt: number;
|
||||||
|
questionList: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从快捷问题题库中随机抽取指定数量的问题。 */
|
||||||
|
export function getRandomAiQuickQuestions(count = AI_QUICK_QUESTION_COUNT, questionPool: readonly string[] = []): string[] {
|
||||||
|
const result: string[] = [...questionPool];
|
||||||
|
const targetCount = Math.min(Math.max(Math.floor(count), 0), result.length);
|
||||||
|
|
||||||
|
for (let index = 0; index < targetCount; index += 1) {
|
||||||
|
const randomIndex = index + Math.floor(Math.random() * (result.length - index));
|
||||||
|
[result[index], result[randomIndex]] = [result[randomIndex], result[index]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.slice(0, targetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清理并去重快捷问题。 */
|
||||||
|
function normalizeAiQuickQuestionList(value: unknown): string[] {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(value.filter((item): item is string => typeof item === "string").map(item => item.trim()).filter(Boolean))];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将 Setting 中的题库值解析为有效的快捷问题列表。 */
|
||||||
|
function parseAiQuickQuestionList(value: unknown): string[] {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return normalizeAiQuickQuestionList(JSON.parse(value));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取一天内有效的本地题库缓存。 */
|
||||||
|
function readAiQuickQuestionCache(): string[] {
|
||||||
|
try {
|
||||||
|
const cacheValue = localStorage.getItem(AI_QUICK_QUESTION_CACHE_KEY);
|
||||||
|
if (!cacheValue) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache: unknown = JSON.parse(cacheValue);
|
||||||
|
if (typeof cache !== "object" || cache === null || !("cachedAt" in cache) || !("questionList" in cache)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const typedCache = cache as AiQuickQuestionCache;
|
||||||
|
if (typeof typedCache.cachedAt !== "number" || !Number.isFinite(typedCache.cachedAt)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheAge = Date.now() - typedCache.cachedAt;
|
||||||
|
if (cacheAge < 0 || AI_QUICK_QUESTION_CACHE_TTL < cacheAge || !Array.isArray(typedCache.questionList)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeAiQuickQuestionList(typedCache.questionList);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将动态题库缓存到本地一天。 */
|
||||||
|
function writeAiQuickQuestionCache(questionList: string[]): void {
|
||||||
|
try {
|
||||||
|
const cache: AiQuickQuestionCache = {
|
||||||
|
cachedAt: Date.now(),
|
||||||
|
questionList
|
||||||
|
};
|
||||||
|
localStorage.setItem(AI_QUICK_QUESTION_CACHE_KEY, JSON.stringify(cache));
|
||||||
|
} catch {
|
||||||
|
// 本地存储不可用时跳过缓存。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从系统设置中获取动态快捷问题题库。 */
|
||||||
|
async function fetchAiQuickQuestionPool(): Promise<string[]> {
|
||||||
|
const settingMap = await SettingAPI.map("GAO_SYSTEM", undefined, ["AI_AGENT_EXAMPLE"]);
|
||||||
|
return parseAiQuickQuestionList(settingMap.AI_AGENT_EXAMPLE?.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadingPromise: Promise<void> | undefined;
|
||||||
|
let currentAiQuickQuestionPool: string[] = [];
|
||||||
|
|
||||||
|
/** 从已加载的题库中重新抽取快捷问题。 */
|
||||||
|
export function refreshAiQuickQuestions(): void {
|
||||||
|
aiQuickQuestionList.value = getRandomAiQuickQuestions(AI_QUICK_QUESTION_COUNT, currentAiQuickQuestionPool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载动态题库并刷新当前会话展示的问题。 */
|
||||||
|
export async function loadAiQuickQuestions(): Promise<void> {
|
||||||
|
if (loadingPromise) {
|
||||||
|
await loadingPromise;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingPromise = (async () => {
|
||||||
|
const cachedQuestionList = readAiQuickQuestionCache();
|
||||||
|
if (cachedQuestionList.length > 0) {
|
||||||
|
currentAiQuickQuestionPool = cachedQuestionList;
|
||||||
|
refreshAiQuickQuestions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAiQuickQuestionPool = [];
|
||||||
|
aiQuickQuestionList.value = [];
|
||||||
|
try {
|
||||||
|
const questionList = await fetchAiQuickQuestionPool();
|
||||||
|
if (questionList.length > 0) {
|
||||||
|
currentAiQuickQuestionPool = questionList;
|
||||||
|
writeAiQuickQuestionCache(questionList);
|
||||||
|
refreshAiQuickQuestions();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 接口异常时保持为空。
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadingPromise;
|
||||||
|
} finally {
|
||||||
|
loadingPromise = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前会话欢迎页展示的快捷问题。 */
|
||||||
|
export const aiQuickQuestionList = ref<string[]>([]);
|
||||||
|
|||||||
@@ -16,6 +16,19 @@
|
|||||||
<span v-text="question" />
|
<span v-text="question" />
|
||||||
</t-button>
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
|
<t-button
|
||||||
|
v-if="props.quickQuestionList.length"
|
||||||
|
class="refresh-button"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
:disabled="isRefreshingQuestions"
|
||||||
|
@click="handleRefreshQuestions"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<t-icon name="refresh" class="refresh-icon" :class="{ refreshing: isRefreshingQuestions }" />
|
||||||
|
</template>
|
||||||
|
<span>换一批</span>
|
||||||
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article
|
<article
|
||||||
@@ -65,7 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { MarkdownView } from "timi-web";
|
import { MarkdownView, Toolkit } from "timi-web";
|
||||||
import type { AiChatMessage } from "@/composables/useAiChat";
|
import type { AiChatMessage } from "@/composables/useAiChat";
|
||||||
import AiChatThinking from "@/pages/ai/components/AiChatThinking.vue";
|
import AiChatThinking from "@/pages/ai/components/AiChatThinking.vue";
|
||||||
|
|
||||||
@@ -82,9 +95,21 @@ interface Props {
|
|||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
selectQuestion: [question: string];
|
selectQuestion: [question: string];
|
||||||
|
refreshQuestions: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const messageListRef = useTemplateRef<HTMLElement>("messageList");
|
const messageListRef = useTemplateRef<HTMLElement>("messageList");
|
||||||
|
const isRefreshingQuestions = ref(false);
|
||||||
|
|
||||||
|
async function handleRefreshQuestions(): Promise<void> {
|
||||||
|
if (isRefreshingQuestions.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRefreshingQuestions.value = true;
|
||||||
|
emit("refreshQuestions");
|
||||||
|
await Toolkit.sleep(300);
|
||||||
|
isRefreshingQuestions.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
async function scrollToBottom(): Promise<void> {
|
async function scrollToBottom(): Promise<void> {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -167,17 +192,35 @@ defineExpose({
|
|||||||
font-size: .8125rem;
|
font-size: .8125rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.quick-list {
|
.quick-list {
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
.quick-question {
|
.quick-question {
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
white-space: normal;
|
|
||||||
|
:deep(.t-button__content) {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button {
|
||||||
|
margin-top: .75rem;
|
||||||
|
|
||||||
|
.refresh-icon.refreshing {
|
||||||
|
animation: refresh-once .3s var(--tui-bezier);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,4 +390,13 @@ defineExpose({
|
|||||||
transform: translateY(-.1875rem);
|
transform: translateY(-.1875rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes refresh-once {
|
||||||
|
from {
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
:messages="messages"
|
:messages="messages"
|
||||||
:is-generating="isGenerating"
|
:is-generating="isGenerating"
|
||||||
:quick-question-list="aiQuickQuestionList"
|
:quick-question-list="aiQuickQuestionList"
|
||||||
@select-question="selectQuestion"
|
@select-question="submitMessage"
|
||||||
|
@refresh-questions="refreshQuickQuestions"
|
||||||
/>
|
/>
|
||||||
<div class="bottom-area">
|
<div class="bottom-area">
|
||||||
<div v-if="activeToolStatus" class="tool-status">
|
<div v-if="activeToolStatus" class="tool-status">
|
||||||
@@ -39,7 +40,7 @@ import type { GaoAiConversation } from "@/api/GaoAiAPI";
|
|||||||
import AiChatComposer from "@/pages/ai/components/AiChatComposer.vue";
|
import AiChatComposer from "@/pages/ai/components/AiChatComposer.vue";
|
||||||
import AiConversationList from "@/pages/ai/components/AiConversationList.vue";
|
import AiConversationList from "@/pages/ai/components/AiConversationList.vue";
|
||||||
import AiChatMessageList from "@/pages/ai/components/AiChatMessageList.vue";
|
import AiChatMessageList from "@/pages/ai/components/AiChatMessageList.vue";
|
||||||
import { aiQuickQuestionList } from "@/pages/ai/aiChatQuestions";
|
import { aiQuickQuestionList, loadAiQuickQuestions, refreshAiQuickQuestions } from "@/pages/ai/aiChatQuestions";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
@@ -110,17 +111,18 @@ function handleConversationDelete(deletedConversationId: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectQuestion(question: string): void {
|
function refreshQuickQuestions(): void {
|
||||||
draft.value = question;
|
refreshAiQuickQuestions();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scrollToBottom(): Promise<void> {
|
async function scrollToBottom(): Promise<void> {
|
||||||
await messageListRef.value?.scrollToBottom();
|
await messageListRef.value?.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
navBarStore.clearRight();
|
navBarStore.clearRight();
|
||||||
navBarStore.setRightText("历史", "var(--td-brand-color)", openConversationList);
|
navBarStore.setRightText("历史", "var(--td-brand-color)", openConversationList);
|
||||||
|
await loadAiQuickQuestions();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(isBusy, value => navBarStore.setRightDisabled(value));
|
watch(isBusy, value => navBarStore.setRightDisabled(value));
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<span class="title" v-text="getConversationName(item)" />
|
<span class="title" v-text="getConversationName(item)" />
|
||||||
</template>
|
</template>
|
||||||
<template #description>
|
<template #description>
|
||||||
<span v-text="formatConversationTime(item)" />
|
<pass-time-label :timestamp="formatConversationTime(item)" underline />
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
<template #right>
|
<template #right>
|
||||||
@@ -78,6 +78,7 @@ import { DialogPlugin } from "tdesign-mobile-vue";
|
|||||||
import { deleteConversation as removeConversation, type GaoAiConversation, listConversations } from "@/api/GaoAiAPI";
|
import { deleteConversation as removeConversation, type GaoAiConversation, listConversations } from "@/api/GaoAiAPI";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
|
import { PassTimeLabel } from "timi-web";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "AiConversationList"
|
name: "AiConversationList"
|
||||||
@@ -114,16 +115,6 @@ const deletingId = ref("");
|
|||||||
let requestToken = 0;
|
let requestToken = 0;
|
||||||
let loadingPromise: Promise<void> | undefined;
|
let loadingPromise: Promise<void> | undefined;
|
||||||
|
|
||||||
const loadStateText = computed(() => {
|
|
||||||
if (isLoading.value && conversationList.value.length) {
|
|
||||||
return "正在加载更多";
|
|
||||||
}
|
|
||||||
if (isNoMore.value) {
|
|
||||||
return conversationList.value.length ? "没有更多会话" : "";
|
|
||||||
}
|
|
||||||
return "上拉加载更多";
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadMore(): Promise<void> {
|
async function loadMore(): Promise<void> {
|
||||||
if (loadingPromise) {
|
if (loadingPromise) {
|
||||||
await loadingPromise;
|
await loadingPromise;
|
||||||
@@ -240,14 +231,13 @@ function getConversationName(conversation: GaoAiConversation): string {
|
|||||||
return conversation.name?.trim() || conversation.introduction?.trim() || "未命名会话";
|
return conversation.name?.trim() || conversation.introduction?.trim() || "未命名会话";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatConversationTime(conversation: GaoAiConversation): string {
|
function formatConversationTime(conversation: GaoAiConversation): number {
|
||||||
const value = conversation["updated_at"] || conversation["created_at"];
|
const value = conversation["updated_at"] || conversation["created_at"];
|
||||||
const timestamp = typeof value === "number" ? value : Number(value);
|
const timestamp = typeof value === "number" ? value : Number(value);
|
||||||
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
||||||
return "时间未知";
|
return "时间未知";
|
||||||
}
|
}
|
||||||
const milliseconds = timestamp < 100000000000 ? timestamp * 1000 : timestamp;
|
return timestamp < 100000000000 ? timestamp * 1000 : timestamp;
|
||||||
return new Date(milliseconds).toLocaleString("zh-CN", { hour12: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.visible, async visible => {
|
watch(() => props.visible, async visible => {
|
||||||
|
|||||||
Reference in New Issue
Block a user