|
|
@@ -1,52 +1,136 @@
|
|
|
<template>
|
|
|
- <FlexCol v-if="showSessionSidebar" position="relative" width="100%">
|
|
|
- <NavBar
|
|
|
- title="历史会话"
|
|
|
- leftButton="close-bold"
|
|
|
- @leftButtonPressed="showSessionSidebar=false"
|
|
|
- />
|
|
|
- <ChatSessionSidebar :sessionManager="sessionManager" @close="showSessionSidebar = false" />
|
|
|
- </FlexCol>
|
|
|
- <ChatMessageContainer
|
|
|
- v-else
|
|
|
- :chatManager="chatManager"
|
|
|
- :sessionManager="sessionManager"
|
|
|
- :historyItemsPagerManager="historyItemsPagerManager"
|
|
|
- :chatInterfaceManager="interfaceManager"
|
|
|
- @intoSelectMode="intoSelectMode"
|
|
|
+ <!-- 主弹窗 -->
|
|
|
+ <Popup
|
|
|
+ :show="showAgentPopup && !isAnySubDialogShow"
|
|
|
+ position="bottom"
|
|
|
+ :mask="false"
|
|
|
+ size="70vh"
|
|
|
+ @update:show="emit('update:showAgentPopup', $event)"
|
|
|
>
|
|
|
- <template #header>
|
|
|
+ <FlexCol v-if="showSessionSidebar" position="relative" width="100%">
|
|
|
<NavBar
|
|
|
- title="AI伴写"
|
|
|
- leftButton="menu"
|
|
|
- rightButton="close-bold"
|
|
|
- @leftButtonPressed="showSessionSidebar = true"
|
|
|
- @rightButtonPressed="emit('close')"
|
|
|
+ title="历史会话"
|
|
|
+ leftButton="close-bold"
|
|
|
+ @leftButtonPressed="showSessionSidebar=false"
|
|
|
/>
|
|
|
- </template>
|
|
|
- <template #footer>
|
|
|
- <ChatFooter
|
|
|
- :chatManager="chatManager"
|
|
|
- :chatInterfaceManager="interfaceManager"
|
|
|
- >
|
|
|
- <template #mulitSelectMode>
|
|
|
- <ChatMulitSelectBar
|
|
|
- :selectedCount="selectedCount"
|
|
|
- :messages="messages"
|
|
|
- :sessionManager="sessionManager"
|
|
|
- @cancel="exitSelectMode"
|
|
|
- />
|
|
|
- </template>
|
|
|
- <template #header>
|
|
|
-
|
|
|
- </template>
|
|
|
- </ChatFooter>
|
|
|
- </template>
|
|
|
- </ChatMessageContainer>
|
|
|
+ <ChatSessionSidebar :sessionManager="sessionManager" @close="showSessionSidebar = false" />
|
|
|
+ </FlexCol>
|
|
|
+ <ChatMessageContainer
|
|
|
+ v-else
|
|
|
+ :chatManager="chatManager"
|
|
|
+ :sessionManager="sessionManager"
|
|
|
+ :historyItemsPagerManager="historyItemsPagerManager"
|
|
|
+ :chatInterfaceManager="interfaceManager"
|
|
|
+ @intoSelectMode="intoSelectMode"
|
|
|
+ >
|
|
|
+ <template #header>
|
|
|
+ <NavBar
|
|
|
+ title="AI伴写"
|
|
|
+ leftButton="menu"
|
|
|
+ rightButton="close-bold"
|
|
|
+ @leftButtonPressed="showSessionSidebar = true"
|
|
|
+ @rightButtonPressed="emit('close')"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <template #footer>
|
|
|
+ <ChatFooter
|
|
|
+ :chatManager="chatManager"
|
|
|
+ :chatInterfaceManager="interfaceManager"
|
|
|
+ >
|
|
|
+ <template #mulitSelectMode>
|
|
|
+ <ChatMulitSelectBar
|
|
|
+ :selectedCount="selectedCount"
|
|
|
+ :messages="messages"
|
|
|
+ :sessionManager="sessionManager"
|
|
|
+ @cancel="exitSelectMode"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <template #header>
|
|
|
+ <scroll-view scroll-x>
|
|
|
+ <FlexRow align="center" padding="padding.md" gap="gap.md">
|
|
|
+ <Button v-if="showImageWritingBtn" text="看图写作" @click="showImageWritingDialog = true" />
|
|
|
+ <Button v-if="showContinueBtn" text="AI续写" @click="showContinueDialog = true" />
|
|
|
+ <Button v-if="showPolishBtn" text="AI润色" @click="showPolishDialog = true" />
|
|
|
+ <Button v-if="showCorrectBtn" text="AI订正" @click="confirmCorrect" />
|
|
|
+ <Button v-if="showSummaryBtn" text="AI总结" @click="confirmSummary" />
|
|
|
+ </FlexRow>
|
|
|
+ </scroll-view>
|
|
|
+ </template>
|
|
|
+ </ChatFooter>
|
|
|
+ </template>
|
|
|
+ </ChatMessageContainer>
|
|
|
+ </Popup>
|
|
|
+
|
|
|
+ <!-- 看图写作弹窗 -->
|
|
|
+ <Popup
|
|
|
+ v-model:show="showImageWritingDialog"
|
|
|
+ closeable
|
|
|
+ :mask="false"
|
|
|
+ position="bottom"
|
|
|
+ >
|
|
|
+ <NavBar
|
|
|
+ leftButton="direction-left"
|
|
|
+ title="看图写作要求"
|
|
|
+ @leftButtonPressed="showImageWritingDialog = false"
|
|
|
+ />
|
|
|
+ <FlexCol padding="padding.md" gap="gap.md">
|
|
|
+ <Field label="篇幅">
|
|
|
+ <Tags v-model="imageWritingLength" :tags="imageWritingLengthOptions" />
|
|
|
+ </Field>
|
|
|
+ <Field label="其他要求" v-model="imageWritingContent" placeholder="输入其他要求" />
|
|
|
+ <Button type="primary" text="开始看图写作" @click="confirmImageWriting" />
|
|
|
+ </FlexCol>
|
|
|
+ </Popup>
|
|
|
+
|
|
|
+ <!-- AI续写弹窗 -->
|
|
|
+ <Popup
|
|
|
+ v-model:show="showContinueDialog"
|
|
|
+ closeable
|
|
|
+ :mask="false"
|
|
|
+ position="bottom"
|
|
|
+ >
|
|
|
+ <NavBar
|
|
|
+ leftButton="direction-left"
|
|
|
+ title="AI续写要求"
|
|
|
+ @leftButtonPressed="showContinueDialog = false"
|
|
|
+ />
|
|
|
+ <FlexCol padding="padding.md" gap="gap.md">
|
|
|
+ <Field label="篇幅">
|
|
|
+ <Tags v-model="continueLength" :tags="continueLengthOptions" />
|
|
|
+ </Field>
|
|
|
+ <Field label="更多">
|
|
|
+ <Tags v-model="continueStyle" :tags="continueStyleOptions" />
|
|
|
+ </Field>
|
|
|
+ <Field label="其他要求" v-model="continueExtra" placeholder="输入其他要求" />
|
|
|
+ <Button type="primary" text="开始AI续写" @click="confirmContinue" />
|
|
|
+ </FlexCol>
|
|
|
+ </Popup>
|
|
|
+
|
|
|
+ <!-- AI润色弹窗 -->
|
|
|
+ <Popup
|
|
|
+ v-model:show="showPolishDialog"
|
|
|
+ closeable
|
|
|
+ :mask="false"
|
|
|
+ position="bottom"
|
|
|
+ >
|
|
|
+ <NavBar
|
|
|
+ leftButton="direction-left"
|
|
|
+ title="AI润色要求"
|
|
|
+ @leftButtonPressed="showPolishDialog = false"
|
|
|
+ />
|
|
|
+ <FlexCol padding="padding.md" gap="gap.md">
|
|
|
+ <Field label="润色方向">
|
|
|
+ <Tags v-model="polishStyle" :tags="polishStyleOptions" />
|
|
|
+ </Field>
|
|
|
+ <Field label="其他要求" v-model="polishExtra" placeholder="输入其他要求" />
|
|
|
+ <Button type="primary" text="开始AI润色" @click="confirmPolish" />
|
|
|
+ </FlexCol>
|
|
|
+ </Popup>
|
|
|
+
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { onMounted, ref, type Ref } from 'vue';
|
|
|
+import { computed, onMounted, ref, type Ref } from 'vue';
|
|
|
import { useChatSession } from '../../../composables/useChatSession';
|
|
|
import { useChat, type ChatInterfaceManager } from '../../../core/Chat';
|
|
|
import { useChatHistoryItemsPager } from '../../../composables/useChatHistoryItemsPager';
|
|
|
@@ -60,6 +144,14 @@ import ChatSessionSidebar from '../../../components/Session/ChatSessionSidebar.v
|
|
|
import ChatMulitSelectBar from '../../../components/Footer/ChatMulitSelectBar.vue';
|
|
|
import NavBar from '@/components/nav/NavBar.vue';
|
|
|
import FlexCol from '@/components/layout/FlexCol.vue';
|
|
|
+import FlexRow from '@/components/layout/FlexRow.vue';
|
|
|
+import type { VillageListItem } from '@/api/light/LightVillageApi';
|
|
|
+import AgentApi from '@/api/agent/Agent';
|
|
|
+import CommonContent from '@/api/CommonContent';
|
|
|
+import Button from '@/components/basic/Button.vue';
|
|
|
+import Field from '@/components/form/Field.vue';
|
|
|
+import Popup from '@/components/dialog/Popup.vue';
|
|
|
+import Tags from '@/components/form/Tags.vue';
|
|
|
|
|
|
const messages = ref<ChatMessageModel[]>([]) as Ref<ChatMessageModel[]>;
|
|
|
const interfaceManager: ChatInterfaceManager = {
|
|
|
@@ -79,17 +171,198 @@ const props = defineProps<{
|
|
|
url: string;
|
|
|
localUrl: string;
|
|
|
}[];
|
|
|
+ villageInfo?: VillageListItem | undefined;
|
|
|
+ tag?: string;
|
|
|
+ showAgentPopup: boolean;
|
|
|
}>();
|
|
|
const emit = defineEmits([
|
|
|
'update:title',
|
|
|
'update:content',
|
|
|
'update:images',
|
|
|
+ 'update:showAgentPopup',
|
|
|
'close',
|
|
|
'upload'
|
|
|
]);
|
|
|
|
|
|
const showSessionSidebar = ref(false);
|
|
|
|
|
|
+//快速操作控制
|
|
|
+//=============================================
|
|
|
+
|
|
|
+const showImageWritingDialog = ref(false);
|
|
|
+const imageWritingContent = ref('');
|
|
|
+const imageWritingLength = ref(300);
|
|
|
+const imageWritingLengthOptions = [
|
|
|
+ { text: '约50字', value: 50 },
|
|
|
+ { text: '约100字', value: 100 },
|
|
|
+ { text: '约300字', value: 300 },
|
|
|
+ { text: '约500字', value: 500 },
|
|
|
+];
|
|
|
+
|
|
|
+function confirmImageWriting() {
|
|
|
+ showImageWritingDialog.value = false;
|
|
|
+ if (props.content.length > 0) {
|
|
|
+ chatManager.send(`根据图片描述为我续写,约${imageWritingLength.value}字${imageWritingContent.value ? ',' + imageWritingContent.value : ''}`);
|
|
|
+ } else {
|
|
|
+ chatManager.send(`帮我看图图写作,约${imageWritingLength.value}字${imageWritingContent.value ? ',' + imageWritingContent.value : ''}`);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const showContinueDialog = ref(false);
|
|
|
+const continueLength = ref(100);
|
|
|
+const continueLengthOptions = [
|
|
|
+ { text: '约50字', value: 50 },
|
|
|
+ { text: '约100字', value: 100 },
|
|
|
+ { text: '约300字', value: 300 },
|
|
|
+];
|
|
|
+const continueStyle = ref('');
|
|
|
+const continueStyleOptions = [
|
|
|
+ { text: '发散介绍', value: '发散介绍' },
|
|
|
+ { text: '详细论述', value: '详细论述' },
|
|
|
+ { text: '添加转折', value: '添加转折' },
|
|
|
+ { text: '总结升华', value: '总结升华' },
|
|
|
+];
|
|
|
+const continueExtra = ref('');
|
|
|
+
|
|
|
+function confirmContinue() {
|
|
|
+ showContinueDialog.value = false;
|
|
|
+ const parts = [`帮我续写文章,约${continueLength.value}字`];
|
|
|
+ if (continueStyle.value) parts.push(`风格:${continueStyle.value}`);
|
|
|
+ if (continueExtra.value) parts.push(continueExtra.value);
|
|
|
+ chatManager.send(parts.join(','));
|
|
|
+}
|
|
|
+
|
|
|
+const showPolishDialog = ref(false);
|
|
|
+const polishStyle = ref('');
|
|
|
+const polishStyleOptions = [
|
|
|
+ { text: '语句通顺', value: '使语句更加通顺流畅' },
|
|
|
+ { text: '文采提升', value: '提升文采,使用更优美的表达' },
|
|
|
+ { text: '口语化', value: '改为口语化、亲切自然的表达' },
|
|
|
+ { text: '正式书面', value: '改为正式书面语风格' },
|
|
|
+];
|
|
|
+const polishExtra = ref('');
|
|
|
+
|
|
|
+function confirmPolish() {
|
|
|
+ showPolishDialog.value = false;
|
|
|
+ const parts = ['帮我润色文章'];
|
|
|
+ if (polishStyle.value) parts.push(`要求:${polishStyle.value}`);
|
|
|
+ if (polishExtra.value) parts.push(polishExtra.value);
|
|
|
+ chatManager.send(parts.join(','));
|
|
|
+}
|
|
|
+
|
|
|
+function confirmCorrect() {
|
|
|
+ chatManager.send('帮我检查文章中的错别字与语病并修正');
|
|
|
+}
|
|
|
+
|
|
|
+function confirmSummary() {
|
|
|
+ chatManager.send('帮我总结文章中的内容');
|
|
|
+}
|
|
|
+
|
|
|
+const isAnySubDialogShow = computed(() =>
|
|
|
+ showImageWritingDialog.value || showContinueDialog.value || showPolishDialog.value
|
|
|
+);
|
|
|
+
|
|
|
+/**
|
|
|
+ - 看图写作 — 有上传图片时显示
|
|
|
+ - AI续写 — 有图片或正文超 8 字时显示
|
|
|
+ - AI润色 / AI总结 — 正文超 50 字时显示
|
|
|
+ - AI订正 — 正文超 20 字时显示
|
|
|
+ */
|
|
|
+const showImageWritingBtn = computed(() => props.images.length > 0);
|
|
|
+const showContinueBtn = computed(() => props.images.length > 0 || props.content.length > 8);
|
|
|
+const showPolishBtn = computed(() => props.content.length > 50);
|
|
|
+const showCorrectBtn = computed(() => props.content.length > 20);
|
|
|
+const showSummaryBtn = computed(() => props.content.length > 50);
|
|
|
+
|
|
|
+//图片识别
|
|
|
+//=============================================
|
|
|
+
|
|
|
+const uploadedImageDescs = [] as {
|
|
|
+ srcImage: string;
|
|
|
+ aiDescription: string;
|
|
|
+}[];
|
|
|
+
|
|
|
+/**
|
|
|
+ * 为用户上传的图片生成AI描述,以供写作助手使用。
|
|
|
+ * 函数将进行去重处理,不会重复生成相同的描述。
|
|
|
+ */
|
|
|
+async function generateAiImageDescs() {
|
|
|
+ if (props.images.length === 0) return;
|
|
|
+
|
|
|
+ const newImages = props.images.filter(
|
|
|
+ img => !uploadedImageDescs.some(desc => desc.srcImage === img.localUrl)
|
|
|
+ );
|
|
|
+ if (newImages.length === 0) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ uni.showLoading({ title: 'AI识图中...' });
|
|
|
+
|
|
|
+ const uploadedUrls = await Promise.all(
|
|
|
+ newImages.map(async (img) => {
|
|
|
+ if (img.url) return { localUrl: img.localUrl, remoteUrl: img.url };
|
|
|
+ const res = await CommonContent.uploadFile(img.localUrl, 'image', 'file');
|
|
|
+ return { localUrl: img.localUrl, remoteUrl: res.fullurl };
|
|
|
+ })
|
|
|
+ );
|
|
|
+
|
|
|
+ const imageContents = uploadedUrls.flatMap(item => ([
|
|
|
+ { type: "image_url", image_url: { url: item.remoteUrl } },
|
|
|
+ ]));
|
|
|
+
|
|
|
+ const res = await AgentApi.chat({
|
|
|
+ model: "hy-vision-2.0-instruct",
|
|
|
+ messages: [
|
|
|
+ {
|
|
|
+ role: "system",
|
|
|
+ content: "你是一个图片解析助手,请依次描述每张图片的内容,用于辅助乡村文化写作。" +
|
|
|
+ "请按以下JSON数组格式输出,不要输出其他内容:\n[{\"index\":1,\"scene\":\"场景概述\",\"details\":\"细节描述\",\"mood\":\"氛围/情感基调\"}]",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ role: "user",
|
|
|
+ content: [
|
|
|
+ ...imageContents,
|
|
|
+ {
|
|
|
+ type: "text",
|
|
|
+ text: '请依次描述每张图片的内容'
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ ] as any,
|
|
|
+ stream: false,
|
|
|
+ });
|
|
|
+
|
|
|
+ const content = res.data?.choices?.[0]?.message?.content || '';
|
|
|
+ try {
|
|
|
+ const jsonMatch = content.match(/\[[\s\S]*\]/);
|
|
|
+ if (jsonMatch) {
|
|
|
+ const descriptions: { index: number; scene: string; details: string; mood: string }[] = JSON.parse(jsonMatch[0]);
|
|
|
+ descriptions.forEach((desc, i) => {
|
|
|
+ if (i < uploadedUrls.length) {
|
|
|
+ uploadedImageDescs.push({
|
|
|
+ srcImage: uploadedUrls[i].localUrl,
|
|
|
+ aiDescription: `${desc.scene}。${desc.details}。氛围:${desc.mood}`,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ uploadedUrls.forEach(item => {
|
|
|
+ uploadedImageDescs.push({
|
|
|
+ srcImage: item.localUrl,
|
|
|
+ aiDescription: content,
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('图片AI描述生成失败', error);
|
|
|
+ } finally {
|
|
|
+ uni.hideLoading();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+//AI 聊天控制
|
|
|
+//=============================================
|
|
|
+
|
|
|
const sessionManager = useChatSession({
|
|
|
messages,
|
|
|
enableSession: true,
|
|
|
@@ -102,22 +375,25 @@ const historyItemsPagerManager = useChatHistoryItemsPager({
|
|
|
sessionManager,
|
|
|
});
|
|
|
|
|
|
-const { registerTools } = useAgentTools({
|
|
|
- title: props.title,
|
|
|
- content: props.content,
|
|
|
- images: props.images,
|
|
|
- onUpdateTitle: (title) => emit('update:title', title),
|
|
|
- onUpdateContent: (content) => emit('update:content', content),
|
|
|
- onUpdateImages: (images) => emit('update:images', images),
|
|
|
+
|
|
|
+const titleRef = computed({
|
|
|
+ get: () => props.title,
|
|
|
+ set: (v: string) => emit('update:title', v),
|
|
|
+});
|
|
|
+const contentRef = computed({
|
|
|
+ get: () => props.content,
|
|
|
+ set: (v: string) => emit('update:content', v),
|
|
|
+});
|
|
|
+const imagesRef = computed({
|
|
|
+ get: () => props.images,
|
|
|
+ set: (v: { url: string; localUrl: string }[]) => emit('update:images', v as any),
|
|
|
});
|
|
|
|
|
|
-const welcomeActions = [
|
|
|
- '乡村振兴的现状与未来',
|
|
|
- '乡源情怀是什么',
|
|
|
- '乡源文化的传承与发展',
|
|
|
- '乡源文化的保护与利用',
|
|
|
- '乡源文化的传承与发展',
|
|
|
-]
|
|
|
+const { registerTools } = useAgentTools({
|
|
|
+ title: titleRef,
|
|
|
+ content: contentRef,
|
|
|
+ images: imagesRef,
|
|
|
+});
|
|
|
|
|
|
const chatManager = useChat({
|
|
|
interfaceManager,
|
|
|
@@ -149,17 +425,36 @@ const chatManager = useChat({
|
|
|
## 强制规则(很重要)
|
|
|
- 当用户的需求涉及“基于现有文章内容”进行判断/总结/改写/润色/纠错/扩写时:你必须先调用工具读取文章(至少读取标题或正文)再开始。
|
|
|
- 当需要改动文章时:优先使用“片段替换/插入/删除”等精确编辑工具;只有在大改(重写/大幅重排)时才使用整篇覆盖写入。
|
|
|
-- 任何工具写入后,都要简短说明你改了什么(1-3 条),避免长篇复述全文。
|
|
|
- 如果用户只是在聊天,不需要动文章,就不要调用写入工具。
|
|
|
- 用户大部分情况下需要简短的文章,请尽量在200~800字以内完成任务。
|
|
|
|
|
|
## 输出偏好
|
|
|
-- 先给结论/方案,再给要点;结构清晰,使用小标题与列表。
|
|
|
-- 涉及改稿时,优先给“可直接应用”的修改结果(通过工具写入完成),必要时补充原因。`,
|
|
|
+- 输出纯文本,除段落空行或首段空格外,**不要包含markdown** 。
|
|
|
+- 先给结论/方案,再给要点;结构清晰。`,
|
|
|
+ onGetAppendSystemMessages: () => {
|
|
|
+ const result = [] as string[];
|
|
|
+ result.push('## 编写内容');
|
|
|
+ result.push('用户正在编写话题:' + props.tag);
|
|
|
+ if (props.villageInfo) {
|
|
|
+ result.push('用户编写内容与此村社有关:' + props.villageInfo.name);
|
|
|
+ result.push('地址:' + props.villageInfo.address);
|
|
|
+ }
|
|
|
+ if (uploadedImageDescs.length > 0) {
|
|
|
+ result.push('## 相关图片');
|
|
|
+ result.push('用户上传了以下图片,请参考图片说明内容进行写作:');
|
|
|
+ uploadedImageDescs.forEach((desc, i) => {
|
|
|
+ result.push(`图片${i + 1}:${desc.aiDescription}`);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ },
|
|
|
+ onBeforeSend: async (userMessages, streamOptions) => {
|
|
|
+ await generateAiImageDescs();
|
|
|
+ },
|
|
|
onBuildWelcome: () => {
|
|
|
return {
|
|
|
welcomeMessage: '您好!欢迎使用亮乡源AI伴写助手。我可以与您讨论写作灵感、写作计划、为您智能完成写作任务',
|
|
|
- welcomeActions: [...welcomeActions].sort(() => Math.random() - 0.5).slice(0, 3),
|
|
|
+ welcomeActions: [],
|
|
|
}
|
|
|
},
|
|
|
onInitTools: (toolsManager) => {
|