|
@@ -1,5 +1,5 @@
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, nextTick, onMounted, computed, onUnmounted, reactive, watch } from 'vue'
|
|
|
+import { ref, nextTick, onMounted, computed, onUnmounted, reactive } from 'vue'
|
|
|
import { Local } from '/@/utils/storage'
|
|
|
import { User, ChatDotRound, Delete, Edit, Check, Close } from '@element-plus/icons-vue'
|
|
|
import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
|
|
@@ -69,6 +69,9 @@ const toolOptions = ref([
|
|
|
},
|
|
|
])
|
|
|
|
|
|
+const prompt = ref<string>('')
|
|
|
+const openPromptDialog = ref(false)
|
|
|
+
|
|
|
// 模型选择
|
|
|
const modelOptions = ref([
|
|
|
{ label: 'Claude Sonnet 4', value: 'claude-sonnet-4' },
|
|
@@ -86,6 +89,11 @@ onUnmounted(() => chatInstance.value?.())
|
|
|
// 是否正在对话
|
|
|
const isConversationActive = computed(() => chatInstance.value !== undefined)
|
|
|
|
|
|
+const clearMessage = () => {
|
|
|
+ stopConversation()
|
|
|
+ messages.value = []
|
|
|
+}
|
|
|
+
|
|
|
// 发送消息
|
|
|
const sendMessage = () => {
|
|
|
if (!inputMessage.value.trim()) return
|
|
@@ -111,7 +119,15 @@ const sendMessage = () => {
|
|
|
// let toolcall: { name: string; param?: string; value?: string } | undefined = undefined
|
|
|
chatInstance.value = assist.chat({
|
|
|
chatRequest: {
|
|
|
- message: messages.value,
|
|
|
+ message: prompt.value ? [
|
|
|
+ {
|
|
|
+ id: 0,
|
|
|
+ role: 'system',
|
|
|
+ content: prompt.value,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ },
|
|
|
+ ...messages.value,
|
|
|
+ ]: messages.value
|
|
|
},
|
|
|
onReceive: (resp: ChatResponse) => {
|
|
|
switch (resp.type) {
|
|
@@ -154,60 +170,11 @@ ${resp.response.data.replace('\n', '')}
|
|
|
|
|
|
`
|
|
|
}
|
|
|
+ rtn.content += '\n'
|
|
|
chatInstance.value = undefined
|
|
|
},
|
|
|
})
|
|
|
messages.value.push(rtn)
|
|
|
-
|
|
|
- //
|
|
|
- // // 添加用户消息
|
|
|
- // messages.value.push({
|
|
|
- // id: Date.now(),
|
|
|
- // type: 'user',
|
|
|
- // content: inputMessage.value,
|
|
|
- // timestamp: new Date(),
|
|
|
- // })
|
|
|
- //
|
|
|
- // //清空用户聊天框
|
|
|
- // const userMessage = inputMessage.value
|
|
|
- // inputMessage.value = ''
|
|
|
- //
|
|
|
- // // 滚动到底部
|
|
|
- // scrollToBottom()
|
|
|
- //
|
|
|
- // const ai = reactive<Message>({
|
|
|
- // id: Date.now()+1,
|
|
|
- // type: 'ai',
|
|
|
- // content: '',
|
|
|
- // timestamp: new Date()
|
|
|
- // })
|
|
|
- //
|
|
|
- // // 添加一个回复
|
|
|
- // messages.value.push(ai)
|
|
|
- //
|
|
|
- // const delay = async (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
|
|
|
- //
|
|
|
- // const action = async () => {
|
|
|
- // const message = `我收到了您的消息:"${userMessage}"。这是一个示例回复,包含markdown格式的内容。
|
|
|
- //
|
|
|
- // ## 回复内容
|
|
|
- //
|
|
|
- // - 列表项 1
|
|
|
- // - 列表项 2
|
|
|
- // - 列表项 3
|
|
|
- //
|
|
|
- // \`\`\`javascript
|
|
|
- // console.log('这是代码示例');
|
|
|
- // \`\`\``
|
|
|
- // for (let i = 0; i < message.length; i++) {
|
|
|
- // ai.content += message[i]
|
|
|
- // await delay(50)
|
|
|
- // }
|
|
|
- // }
|
|
|
- // action().finally(()=> {
|
|
|
- // isConversationActive.value = false
|
|
|
- // scrollToBottom()
|
|
|
- // })
|
|
|
}
|
|
|
|
|
|
// 终止对话
|
|
@@ -369,12 +336,20 @@ const getUserInfos = ref<{
|
|
|
<div v-if="message.role === 'assistant'" class="ai-message-container">
|
|
|
<el-avatar class="message-avatar" :icon="ChatDotRound" />
|
|
|
<div class="message-bubble ai-bubble">
|
|
|
- <Markdown :content="message.content" :plugins="plugins" class="markdown-content" />
|
|
|
+ <Markdown v-if="message.content !== ''" :content="message.content" :plugins="plugins" class="markdown-content" />
|
|
|
+ <div v-else class="loading-container">
|
|
|
+ <div class="loading-dots">
|
|
|
+ <span class="dot"></span>
|
|
|
+ <span class="dot"></span>
|
|
|
+ <span class="dot"></span>
|
|
|
+ </div>
|
|
|
+ <span class="loading-text">AI正在思考中...</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 用户消息 -->
|
|
|
- <div v-else class="user-message-container">
|
|
|
+ <div v-if="message.role === 'user'" class="user-message-container">
|
|
|
<div class="message-bubble user-bubble">
|
|
|
{{ message.content }}
|
|
|
</div>
|
|
@@ -457,6 +432,18 @@ const getUserInfos = ref<{
|
|
|
<el-option v-for="item in modelOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
</el-select>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 提示词输入框 -->
|
|
|
+ <div class="prompt-selector">
|
|
|
+ <el-input
|
|
|
+ v-model="prompt"
|
|
|
+ placeholder="点击设置提示词..."
|
|
|
+ size="small"
|
|
|
+ style="width: 200px"
|
|
|
+ readonly
|
|
|
+ @click="openPromptDialog = true"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 输入框和按钮行 -->
|
|
@@ -478,6 +465,14 @@ const getUserInfos = ref<{
|
|
|
<!-- 按钮组 -->
|
|
|
<div class="button-group">
|
|
|
<el-button
|
|
|
+ type="warning"
|
|
|
+ size="small"
|
|
|
+ @click="clearMessage"
|
|
|
+ style="margin-left: 12px"
|
|
|
+ >
|
|
|
+ 清空
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
v-if="!isConversationActive"
|
|
|
type="primary"
|
|
|
size="small"
|
|
@@ -493,6 +488,28 @@ const getUserInfos = ref<{
|
|
|
</div>
|
|
|
</el-main>
|
|
|
</el-container>
|
|
|
+
|
|
|
+ <!-- 提示词设置对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="openPromptDialog"
|
|
|
+ title="设置提示词"
|
|
|
+ width="600px"
|
|
|
+ :before-close="() => { openPromptDialog = false }"
|
|
|
+ >
|
|
|
+ <el-input
|
|
|
+ v-model="prompt"
|
|
|
+ type="textarea"
|
|
|
+ placeholder="请输入提示词..."
|
|
|
+ :rows="8"
|
|
|
+ resize="none"
|
|
|
+ />
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="openPromptDialog = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="openPromptDialog = false">确定</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
</template>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
@@ -660,8 +677,8 @@ const getUserInfos = ref<{
|
|
|
background: var(--el-fill-color-light);
|
|
|
color: var(--el-text-color-primary);
|
|
|
position: relative;
|
|
|
- min-width: 50%;
|
|
|
- max-width: 70%;
|
|
|
+ min-width: 70%;
|
|
|
+ max-width: 90%;
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
&::before {
|
|
@@ -718,6 +735,56 @@ const getUserInfos = ref<{
|
|
|
margin-top: 2px;
|
|
|
}
|
|
|
|
|
|
+/* 加载动画样式 */
|
|
|
+.loading-container {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 8px 0;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+}
|
|
|
+
|
|
|
+.loading-dots {
|
|
|
+ display: flex;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.dot {
|
|
|
+ width: 6px;
|
|
|
+ height: 6px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: var(--el-color-primary);
|
|
|
+ animation: loading-bounce 1.4s ease-in-out infinite both;
|
|
|
+}
|
|
|
+
|
|
|
+.dot:nth-child(1) {
|
|
|
+ animation-delay: -0.32s;
|
|
|
+}
|
|
|
+
|
|
|
+.dot:nth-child(2) {
|
|
|
+ animation-delay: -0.16s;
|
|
|
+}
|
|
|
+
|
|
|
+.dot:nth-child(3) {
|
|
|
+ animation-delay: 0s;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes loading-bounce {
|
|
|
+ 0%, 80%, 100% {
|
|
|
+ transform: scale(0.8);
|
|
|
+ opacity: 0.5;
|
|
|
+ }
|
|
|
+ 40% {
|
|
|
+ transform: scale(1);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.loading-text {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
.tools-button {
|
|
|
margin-bottom: 10px;
|
|
|
color: var(--el-text-color-regular);
|
|
@@ -901,7 +968,8 @@ const getUserInfos = ref<{
|
|
|
}
|
|
|
|
|
|
.tool-selector,
|
|
|
-.model-selector {
|
|
|
+.model-selector,
|
|
|
+.prompt-selector {
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
@@ -934,6 +1002,20 @@ const getUserInfos = ref<{
|
|
|
min-width: 60px;
|
|
|
}
|
|
|
|
|
|
+/* 提示词输入框样式 */
|
|
|
+.prompt-selector .el-input {
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.prompt-selector .el-input__inner {
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+/* 对话框样式 */
|
|
|
+.dialog-footer {
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
/* 响应式调整 */
|
|
|
/* 空状态页面样式 */
|
|
|
.empty-content {
|