mirror of
				https://gitee.com/hhyykk/ipms-sjy-ui.git
				synced 2025-11-01 02:38:44 +08:00 
			
		
		
		
	【代码评审】AI:写作相关的建议
This commit is contained in:
		| @@ -3,7 +3,9 @@ import { fetchEventSource } from '@microsoft/fetch-event-source' | ||||
| import { getAccessToken } from '@/utils/auth' | ||||
| import { config } from '@/config/axios/config' | ||||
|  | ||||
| // TODO @hhhero:可以改成 WriteVO 哈,主要是保持一致 | ||||
| export interface WriteParams { | ||||
|   // TODO @hhhero:注释。每个属性的后面哈。会更简洁一点 | ||||
|   /** | ||||
|    * 1:撰写 2:回复 | ||||
|    */ | ||||
| @@ -33,6 +35,7 @@ export interface WriteParams { | ||||
|    */ | ||||
|   language: number | ||||
| } | ||||
|  | ||||
| export const writeStream = ({ | ||||
|   data, | ||||
|   onClose, | ||||
| @@ -46,7 +49,6 @@ export const writeStream = ({ | ||||
|   onClose?: (...args: any[]) => void | ||||
|   ctrl: AbortController | ||||
| }) => { | ||||
|   // return request.post({ url: '/ai/write/generate-stream', data }) | ||||
|   const token = getAccessToken() | ||||
|   return fetchEventSource(`${config.base_url}/ai/write/generate-stream`, { | ||||
|     method: 'post', | ||||
|   | ||||
| @@ -226,7 +226,7 @@ const conversationTimeGroup = async (list: ChatConversationVO[]) => { | ||||
|   const threeDays = 3 * oneDay | ||||
|   const sevenDays = 7 * oneDay | ||||
|   const thirtyDays = 30 * oneDay | ||||
|   for (const conversation: ChatConversationVO of list) { | ||||
|   for (const conversation of list) { | ||||
|     // 置顶 | ||||
|     if (conversation.pinned) { | ||||
|       groupMap['置顶'].push(conversation) | ||||
| @@ -247,7 +247,6 @@ const conversationTimeGroup = async (list: ChatConversationVO[]) => { | ||||
|       groupMap['三十天前'].push(conversation) | ||||
|     } | ||||
|   } | ||||
|   console.log('----groupMap', groupMap) | ||||
|   return groupMap | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <!-- 定义tab组件 --> | ||||
|   <!-- 定义 tab 组件:撰写/回复等 --> | ||||
|   <DefineTab v-slot="{ active, text, itemClick }"> | ||||
|     <span | ||||
|       class="inline-block w-1/2 rounded-full cursor-pointer text-center leading-[30px] relative z-1 text-[5C6370] hover:text-black" | ||||
| @@ -9,7 +9,7 @@ | ||||
|       {{ text }} | ||||
|     </span> | ||||
|   </DefineTab> | ||||
|   <!-- 定义label组件 --> | ||||
|   <!-- 定义 label 组件:长度/格式/语气/语言等 --> | ||||
|   <DefineLabel v-slot="{ label, hint, hintClick }"> | ||||
|     <h3 class="mt-5 mb-3 flex items-center justify-between text-[14px]"> | ||||
|       <span>{{ label }}</span> | ||||
| @@ -23,6 +23,7 @@ | ||||
|       </span> | ||||
|     </h3> | ||||
|   </DefineLabel> | ||||
|  | ||||
|   <!-- TODO 小屏幕的时候是定位在左边的,大屏是分开的 --> | ||||
|   <div class="relative" v-bind="$attrs"> | ||||
|     <!-- tab --> | ||||
| @@ -99,97 +100,102 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
|   import { createReusableTemplate } from '@vueuse/core' | ||||
|   import { ref } from 'vue' | ||||
|   import Tag from './Tag.vue' | ||||
|   import { WriteParams } from '@/api/ai/writer' | ||||
|   import { omit } from 'lodash-es' | ||||
|   import { getIntDictOptions } from '@/utils/dict' | ||||
|   import dataJson from '../data.json' | ||||
| import { createReusableTemplate } from '@vueuse/core' | ||||
| import { ref } from 'vue' | ||||
| import Tag from './Tag.vue' | ||||
| import { WriteParams } from '@/api/ai/writer' | ||||
| import { omit } from 'lodash-es' | ||||
| import { getIntDictOptions } from '@/utils/dict' | ||||
| import dataJson from '../data.json' | ||||
|  | ||||
|   type TabType = WriteParams['type'] | ||||
| type TabType = WriteParams['type'] | ||||
|  | ||||
|   const message = useMessage() | ||||
| const message = useMessage() | ||||
|  | ||||
|   defineProps<{ | ||||
|     isWriting: boolean | ||||
|   }>() | ||||
| defineProps<{ | ||||
|   isWriting: boolean | ||||
| }>() | ||||
|  | ||||
|   const emits = defineEmits<{ | ||||
|     (e: 'submit', params: Partial<WriteParams>) | ||||
|     (e: 'example', param: 'write' | 'reply') | ||||
|   }>() | ||||
| const emits = defineEmits<{ | ||||
|   (e: 'submit', params: Partial<WriteParams>) | ||||
|   (e: 'example', param: 'write' | 'reply') | ||||
| }>() | ||||
|  | ||||
|   const example = (type: 'write' | 'reply') => { | ||||
|     writeForm.value = { | ||||
|       ...initData, | ||||
|       ...omit(dataJson[type], ['data']) | ||||
|     } | ||||
|     emits('example', type) | ||||
| const example = (type: 'write' | 'reply') => { | ||||
|   writeForm.value = { | ||||
|     ...initData, | ||||
|     ...omit(dataJson[type], ['data']) | ||||
|   } | ||||
|   emits('example', type) | ||||
| } | ||||
|  | ||||
|   const selectedTab = ref<TabType>(1) | ||||
|   const tabs: { | ||||
|     text: string | ||||
|     value: TabType | ||||
|   }[] = [ | ||||
|     { text: '撰写', value: 1 }, | ||||
|     { text: '回复', value: 2 } | ||||
|   ] | ||||
|   const [DefineTab, ReuseTab] = createReusableTemplate<{ | ||||
|     active?: boolean | ||||
|     text: string | ||||
|     itemClick: () => void | ||||
|   }>() | ||||
| const selectedTab = ref<TabType>(1) | ||||
| const tabs: { | ||||
|   text: string | ||||
|   value: TabType | ||||
| }[] = [ | ||||
|   { text: '撰写', value: 1 }, // TODO @hhhero:1、2 这个枚举到 constants 里。方便后续万一要调整 | ||||
|   { text: '回复', value: 2 } | ||||
| ] | ||||
| const [DefineTab, ReuseTab] = createReusableTemplate<{ | ||||
|   active?: boolean | ||||
|   text: string | ||||
|   itemClick: () => void | ||||
| }>() | ||||
|  | ||||
|   const initData: WriteParams = { | ||||
|     type: 1, | ||||
|     prompt: '', | ||||
|     originalContent: '', | ||||
|     tone: 1, | ||||
|     language: 1, | ||||
|     length: 1, | ||||
|     format: 1 | ||||
| const initData: WriteParams = { | ||||
|   type: 1, | ||||
|   prompt: '', | ||||
|   originalContent: '', | ||||
|   tone: 1, | ||||
|   language: 1, | ||||
|   length: 1, | ||||
|   format: 1 | ||||
| } | ||||
| // TODO @hhhero:这个字段,要不叫 formData,和其他模块保持一致。然后 initData 和它也更好对应上 | ||||
| const writeForm = ref<WriteParams>({ ...initData }) | ||||
|  | ||||
| // TODO @hhhero:这种一次性的变量,要不直接 vue template 直接调用。目的是:让 ts 这块,更专注逻辑哈。 | ||||
| const writeTags = { | ||||
|   // 长度 TODO @hhhero:注释放在和面哈; | ||||
|   // TODO @hhhero:一般 length 不用缩写哈。更完整会更容易阅读; | ||||
|   lenTags: getIntDictOptions('ai_write_length'), | ||||
|   // 格式 | ||||
|  | ||||
|   formatTags: getIntDictOptions('ai_write_format'), | ||||
|   // 语气 | ||||
|  | ||||
|   toneTags: getIntDictOptions('ai_write_tone'), | ||||
|   // 语言 | ||||
|   langTags: getIntDictOptions('ai_write_language') | ||||
|   // | ||||
| } | ||||
|  | ||||
| // TODO @hhhero:这个写法不错。要不写个简单的注释,我怕很多人不懂哈。 | ||||
| const [DefineLabel, ReuseLabel] = createReusableTemplate<{ | ||||
|   label: string | ||||
|   class?: string | ||||
|   hint?: string | ||||
|   hintClick?: () => void | ||||
| }>() | ||||
|  | ||||
| const switchTab = (value: TabType) => { | ||||
|   selectedTab.value = value | ||||
|   writeForm.value = { ...initData } | ||||
| } | ||||
|  | ||||
| const submit = () => { | ||||
|   if (selectedTab.value === 2 && !writeForm.value.originalContent) { | ||||
|     message.warning('请输入原文') | ||||
|     return | ||||
|   } | ||||
|   const writeForm = ref<WriteParams>({ ...initData }) | ||||
|  | ||||
|   const writeTags = { | ||||
|     // 长度 | ||||
|     lenTags: getIntDictOptions('ai_write_length'), | ||||
|     // 格式 | ||||
|  | ||||
|     formatTags: getIntDictOptions('ai_write_format'), | ||||
|     // 语气 | ||||
|  | ||||
|     toneTags: getIntDictOptions('ai_write_tone'), | ||||
|     // 语言 | ||||
|     langTags: getIntDictOptions('ai_write_language') | ||||
|     // | ||||
|   } | ||||
|  | ||||
|   const [DefineLabel, ReuseLabel] = createReusableTemplate<{ | ||||
|     label: string | ||||
|     class?: string | ||||
|     hint?: string | ||||
|     hintClick?: () => void | ||||
|   }>() | ||||
|  | ||||
|   const switchTab = (value: TabType) => { | ||||
|     selectedTab.value = value | ||||
|     writeForm.value = { ...initData } | ||||
|   } | ||||
|  | ||||
|   const submit = () => { | ||||
|     if (selectedTab.value === 2 && !writeForm.value.originalContent) { | ||||
|       message.warning('请输入原文') | ||||
|       return | ||||
|     } else if (!writeForm.value.prompt) { | ||||
|       message.warning(`请输入${selectedTab.value === 1 ? '写作' : '回复'}内容`) | ||||
|       return | ||||
|     } | ||||
|     emits('submit', { | ||||
|       ...(selectedTab.value === 1 ? omit(writeForm.value, ['originalContent']) : writeForm.value), | ||||
|       type: selectedTab.value | ||||
|     }) | ||||
|   if (!writeForm.value.prompt) { | ||||
|     message.warning(`请输入${selectedTab.value === 1 ? '写作' : '回复'}内容`) | ||||
|     return | ||||
|   } | ||||
|   emits('submit', { | ||||
|     ...(selectedTab.value === 1 ? omit(writeForm.value, ['originalContent']) : writeForm.value), | ||||
|     type: selectedTab.value | ||||
|   }) | ||||
| } | ||||
| </script> | ||||
|   | ||||
| @@ -35,52 +35,52 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
|   import { useClipboard } from '@vueuse/core' | ||||
|   const message = useMessage() | ||||
|   const props = defineProps({ | ||||
|     msg: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     }, | ||||
|     isWriting: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     } | ||||
|   }) | ||||
| import { useClipboard } from '@vueuse/core' | ||||
|  | ||||
|   const emits = defineEmits(['update:msg', 'stopStream']) | ||||
| const message = useMessage() | ||||
| const { copied, copy } = useClipboard() | ||||
|  | ||||
|   const { copied, copy } = useClipboard() | ||||
|  | ||||
|   const compMsg = computed({ | ||||
|     get() { | ||||
|       return props.msg | ||||
|     }, | ||||
|     set(val) { | ||||
|       emits('update:msg', val) | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   const showCopy = computed(() => props.msg && !props.isWriting) | ||||
|  | ||||
|   const inputId = computed(() => getCurrentInstance()?.uid) | ||||
|  | ||||
|   const contentRef = ref<HTMLDivElement>() | ||||
|   defineExpose({ | ||||
|     scrollToBottom() { | ||||
|       contentRef.value?.scrollTo(0, contentRef.value?.scrollHeight) | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   // 点击复制的时候复制msg | ||||
|   const copyMsg = () => { | ||||
|     copy(props.msg) | ||||
| const props = defineProps({ | ||||
|   msg: { | ||||
|     type: String, | ||||
|     default: '' | ||||
|   }, | ||||
|   isWriting: { | ||||
|     type: Boolean, | ||||
|     default: false | ||||
|   } | ||||
| }) | ||||
|  | ||||
|   watch(copied, (val) => { | ||||
|     console.log({ copied: val }) | ||||
|     if (val) { | ||||
|       message.success('复制成功') | ||||
|     } | ||||
|   }) | ||||
| const emits = defineEmits(['update:msg', 'stopStream']) | ||||
|  | ||||
| // TODO @hhhero:是不是 Msg 改成 Content 这种哈。或者 Message。 | ||||
| const compMsg = computed({ | ||||
|   get() { | ||||
|     return props.msg | ||||
|   }, | ||||
|   set(val) { | ||||
|     emits('update:msg', val) | ||||
|   } | ||||
| }) | ||||
|  | ||||
| /** 滚动 */ | ||||
| const contentRef = ref<HTMLDivElement>() | ||||
| defineExpose({ | ||||
|   scrollToBottom() { | ||||
|     contentRef.value?.scrollTo(0, contentRef.value?.scrollHeight) | ||||
|   } | ||||
| }) | ||||
|  | ||||
| /** 点击复制的时候复制内容 */ | ||||
| const showCopy = computed(() => props.msg && !props.isWriting) // 是否展示拷贝 | ||||
| const inputId = computed(() => getCurrentInstance()?.uid) // TODO @hhhero:这个可以写个注释哈 | ||||
| const copyMsg = () => { | ||||
|   copy(props.msg) | ||||
| } | ||||
|  | ||||
| watch(copied, (val) => { | ||||
|   if (val) { | ||||
|     message.success('复制成功') | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|   | ||||
| @@ -13,20 +13,19 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
|   const props = withDefaults( | ||||
|     defineProps<{ | ||||
|       tags: { label: string; value: string }[] | ||||
|       modelValue: string | ||||
|       [k: string]: any | ||||
|     }>(), | ||||
|     { | ||||
|       tags: () => [] | ||||
|     } | ||||
|   ) | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     tags: { label: string; value: string }[] | ||||
|     modelValue: string | ||||
|     [k: string]: any | ||||
|   }>(), | ||||
|   { | ||||
|     tags: () => [] | ||||
|   } | ||||
| ) | ||||
|  | ||||
|   const emits = defineEmits<{ | ||||
|     (e: 'update:modelValue', value: string): void | ||||
|   }>() | ||||
| const emits = defineEmits<{ | ||||
|   (e: 'update:modelValue', value: string): void | ||||
| }>() | ||||
| </script> | ||||
|  | ||||
| <style scoped></style> | ||||
|   | ||||
| @@ -1,61 +1,67 @@ | ||||
| <!-- TODO @hhhero:挪到 write/index/index.vue 里。因为后续会有 write/manager/index.vue 管理内容 --> | ||||
| <template> | ||||
|   <div class="h-[calc(100vh-var(--top-tool-height)-var(--app-footer-height)-40px)] -m-5 flex"> | ||||
|     <Left :is-writing="isWriting" class="h-full" @submit="submit" @example="example" /> | ||||
|     <Left :is-writing="isWriting" class="h-full" @submit="submit" @example="handleExampleClick" /> | ||||
|     <!-- TODO @hhhero:顶部应该有个预览的 header --> | ||||
|     <!-- TODO @hhhero:整个 Right 组件的框,没铺满的感觉? --> | ||||
|     <Right | ||||
|       :is-writing="isWriting" | ||||
|       @stop-stream="stopStream" | ||||
|       ref="rightRef" | ||||
|       class="flex-grow" | ||||
|       v-model:msg="msgResult" | ||||
|       v-model:msg="writeResult" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
|   import Left from './components/Left.vue' | ||||
|   import Right from './components/Right.vue' | ||||
|   import { writeStream } from '@/api/ai/writer' | ||||
|   import dataJson from './data.json' | ||||
| import Left from './components/Left.vue' | ||||
| import Right from './components/Right.vue' | ||||
| // TODO @hhhero:搞成 WriteApi 哈 | ||||
| import { writeStream } from '@/api/ai/writer' | ||||
| // TODO @hhhero:dataJson 放到 ai/utils/utils.ts | ||||
| import dataJson from './data.json' | ||||
|  | ||||
|   const message = useMessage() | ||||
|   const msgResult = ref('') | ||||
|   const isWriting = ref(false) | ||||
| const message = useMessage() | ||||
|  | ||||
|   const abortController = ref<AbortController>() | ||||
| const writeResult = ref('') // 写作结果 | ||||
| const isWriting = ref(false) // 是否正在写作中 | ||||
| const abortController = ref<AbortController>() // // 写作进行中 abort 控制器(控制 stream 写作) | ||||
|  | ||||
|   const stopStream = () => { | ||||
|     abortController.value?.abort() | ||||
|     isWriting.value = false | ||||
|   } | ||||
| /** 停止 stream 生成 */ | ||||
| const stopStream = () => { | ||||
|   abortController.value?.abort() | ||||
|   isWriting.value = false | ||||
| } | ||||
|  | ||||
|   const rightRef = ref<InstanceType<typeof Right>>() | ||||
| /** 执行写作 */ | ||||
| const rightRef = ref<InstanceType<typeof Right>>() | ||||
| const submit = async (data) => { | ||||
|   abortController.value = new AbortController() | ||||
|   writeResult.value = '' | ||||
|   isWriting.value = true | ||||
|   await writeStream({ | ||||
|     data, | ||||
|     onMessage: async (res) => { | ||||
|       const { code, data, msg } = JSON.parse(res.data) | ||||
|       if (code !== 0) { | ||||
|         message.alert(`写作异常! ${msg}`) | ||||
|         stopStream() | ||||
|         return | ||||
|       } | ||||
|       writeResult.value = writeResult.value + data | ||||
|       nextTick(() => { | ||||
|         rightRef.value?.scrollToBottom() | ||||
|       }) | ||||
|     }, | ||||
|     ctrl: abortController.value, | ||||
|     onClose: stopStream, | ||||
|     onError: stopStream // TODO @hhhero: error 的时候,是不是要打印下错误哈 | ||||
|   }) | ||||
| } | ||||
|  | ||||
|   // 点击示例触发 | ||||
|   const example = (type: keyof typeof dataJson) => { | ||||
|     msgResult.value = dataJson[type].data | ||||
|   } | ||||
|  | ||||
|   const submit = async (data) => { | ||||
|     abortController.value = new AbortController() | ||||
|     msgResult.value = '' | ||||
|     isWriting.value = true | ||||
|     writeStream({ | ||||
|       data, | ||||
|       onMessage: async (res) => { | ||||
|         const { code, data, msg } = JSON.parse(res.data) | ||||
|         if (code !== 0) { | ||||
|           message.alert(`写作异常! ${msg}`) | ||||
|           stopStream() | ||||
|           return | ||||
|         } | ||||
|         msgResult.value = msgResult.value + data | ||||
|         nextTick(() => { | ||||
|           rightRef.value?.scrollToBottom() | ||||
|         }) | ||||
|       }, | ||||
|       ctrl: abortController.value, | ||||
|       onClose: stopStream, | ||||
|       onError: stopStream | ||||
|     }) | ||||
|   } | ||||
| /** 点击示例触发 */ | ||||
| const handleExampleClick = (type: keyof typeof dataJson) => { | ||||
|   writeResult.value = dataJson[type].data | ||||
| } | ||||
| </script> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 YunaiV
					YunaiV