499 lines
21 KiB
JavaScript
499 lines
21 KiB
JavaScript
import { W as connection_default, a7 as loading_default, ab as warning_default, i as ElIcon, aa as delete_default, ac as setting_default, h as ElButton, ad as chat_dot_round_default, ae as user_default, af as arrow_down_default, ag as arrow_up_default, ah as copy_document_default, K as refresh_left_default, a0 as ElAlert, y as ElInput, ai as promotion_default, E as ElMessage, D as ElMessageBox } from "./element-plus.CkEW9frc.js";
|
||
import { J as defineComponent, ej as MarkdownIt, ek as markdownItHighlightjs, t as onMounted, ah as onUnmounted, S as openBlock, _ as createElementBlock, a1 as createBaseVNode, $ as createVNode, a0 as withCtx, T as createBlock, o as unref, a3 as normalizeClass, aa as toDisplayString, a9 as createTextVNode, a8 as createCommentVNode, H as Fragment, ay as renderList, a_ as withKeys, aw as withModifiers, r as ref, j as computed, n as nextTick, el as HighlightJS } from "./.pnpm.BW3P1y8f.js";
|
||
import { _ as _export_sfc } from "./_plugin-vue_export-helper.1tPrXgE0.js";
|
||
const _hoisted_1 = { class: "chatgpt-container" };
|
||
const _hoisted_2 = { class: "main-chat" };
|
||
const _hoisted_3 = { class: "chat-navbar" };
|
||
const _hoisted_4 = { class: "navbar-right" };
|
||
const _hoisted_5 = { class: "connection-status" };
|
||
const _hoisted_6 = { class: "status-text" };
|
||
const _hoisted_7 = {
|
||
key: 0,
|
||
class: "welcome-screen"
|
||
};
|
||
const _hoisted_8 = { class: "welcome-content" };
|
||
const _hoisted_9 = { class: "ai-logo" };
|
||
const _hoisted_10 = { class: "example-prompts" };
|
||
const _hoisted_11 = {
|
||
key: 1,
|
||
class: "messages-list"
|
||
};
|
||
const _hoisted_12 = { class: "message-avatar" };
|
||
const _hoisted_13 = {
|
||
key: 0,
|
||
class: "user-avatar"
|
||
};
|
||
const _hoisted_14 = {
|
||
key: 1,
|
||
class: "ai-avatar"
|
||
};
|
||
const _hoisted_15 = { class: "message-content" };
|
||
const _hoisted_16 = { class: "message-header" };
|
||
const _hoisted_17 = { class: "sender-name" };
|
||
const _hoisted_18 = { class: "message-body" };
|
||
const _hoisted_19 = ["innerHTML"];
|
||
const _hoisted_20 = {
|
||
key: 1,
|
||
class: "typing-indicator"
|
||
};
|
||
const _hoisted_21 = {
|
||
key: 0,
|
||
class: "message-actions"
|
||
};
|
||
const _hoisted_22 = {
|
||
key: 2,
|
||
class: "error-banner"
|
||
};
|
||
const _hoisted_23 = { class: "chat-input" };
|
||
const _hoisted_24 = { class: "input-wrapper" };
|
||
const _hoisted_25 = { class: "input-container" };
|
||
const _sfc_main = /* @__PURE__ */ defineComponent({
|
||
__name: "index",
|
||
setup(__props) {
|
||
const md = new MarkdownIt({
|
||
html: true,
|
||
linkify: true,
|
||
typographer: true,
|
||
breaks: true,
|
||
highlight(str, lang) {
|
||
if (lang && HighlightJS.getLanguage(lang)) {
|
||
try {
|
||
return `<pre class="hljs"><code>${HighlightJS.highlight(str, { language: lang, ignoreIllegals: true }).value}</code></pre>`;
|
||
} catch {
|
||
}
|
||
}
|
||
return `<pre class="hljs"><code>${md.utils.escapeHtml(str)}</code></pre>`;
|
||
}
|
||
}).use(markdownItHighlightjs);
|
||
const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
|
||
return self.renderToken(tokens, idx, options, env, self);
|
||
};
|
||
md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
|
||
tokens[idx].attrPush(["target", "_blank"]);
|
||
tokens[idx].attrPush(["rel", "noopener noreferrer"]);
|
||
return defaultRender(tokens, idx, options, env, self);
|
||
};
|
||
const messages = ref([]);
|
||
const inputMessage = ref("");
|
||
const sending = ref(false);
|
||
const isConnected = ref(false);
|
||
const connectionStatus = ref("disconnected");
|
||
const error = ref("");
|
||
const messagesContainer = ref();
|
||
let ws = null;
|
||
const WS_URL = "undefined/api/v1/application/ai/ws";
|
||
const connectionStatusText = computed(() => {
|
||
switch (connectionStatus.value) {
|
||
case "connected":
|
||
return "已连接";
|
||
case "connecting":
|
||
return "连接中...";
|
||
case "disconnected":
|
||
return "未连接";
|
||
default:
|
||
return "未知状态";
|
||
}
|
||
});
|
||
const connectWebSocket = () => {
|
||
if ((ws == null ? void 0 : ws.readyState) === WebSocket.OPEN) {
|
||
return;
|
||
}
|
||
connectionStatus.value = "connecting";
|
||
error.value = "";
|
||
try {
|
||
ws = new WebSocket(WS_URL);
|
||
ws.onopen = () => {
|
||
console.log("WebSocket 连接已建立");
|
||
isConnected.value = true;
|
||
connectionStatus.value = "connected";
|
||
ElMessage.success("连接成功");
|
||
};
|
||
ws.onmessage = (event) => {
|
||
handleWebSocketMessage({ content: event.data });
|
||
};
|
||
ws.onclose = (event) => {
|
||
console.log("WebSocket 连接已关闭", event.code, event.reason);
|
||
isConnected.value = false;
|
||
connectionStatus.value = "disconnected";
|
||
messages.value.forEach((message) => {
|
||
if (message.type === "assistant" && message.loading) {
|
||
message.loading = false;
|
||
message.collapsed = message.content.length > 200;
|
||
}
|
||
});
|
||
};
|
||
ws.onerror = (error2) => {
|
||
console.error("WebSocket 错误:", error2);
|
||
isConnected.value = false;
|
||
connectionStatus.value = "disconnected";
|
||
ElMessage.error("连接失败,请检查服务器状态");
|
||
messages.value.forEach((message) => {
|
||
if (message.type === "assistant" && message.loading) {
|
||
message.loading = false;
|
||
message.collapsed = message.content.length > 200;
|
||
}
|
||
});
|
||
};
|
||
} catch (err) {
|
||
console.error("创建 WebSocket 连接失败:", err);
|
||
connectionStatus.value = "disconnected";
|
||
error.value = "无法创建连接";
|
||
}
|
||
};
|
||
const disconnectWebSocket = () => {
|
||
if (ws) {
|
||
ws.close(1e3, "用户主动断开");
|
||
ws = null;
|
||
}
|
||
isConnected.value = false;
|
||
connectionStatus.value = "disconnected";
|
||
messages.value.forEach((message) => {
|
||
if (message.type === "assistant" && message.loading) {
|
||
message.loading = false;
|
||
}
|
||
});
|
||
};
|
||
const toggleConnection = () => {
|
||
if (isConnected.value) {
|
||
disconnectWebSocket();
|
||
ElMessage.info("已断开连接");
|
||
} else {
|
||
connectWebSocket();
|
||
}
|
||
};
|
||
const handleWebSocketMessage = (data) => {
|
||
const lastMessage = messages.value[messages.value.length - 1];
|
||
if (lastMessage && lastMessage.type === "assistant" && lastMessage.loading) {
|
||
lastMessage.content += data.content || data.message || "";
|
||
} else {
|
||
addMessage("assistant", data.content || data.message || "收到回复");
|
||
}
|
||
scrollToBottom();
|
||
};
|
||
const sendMessage = async () => {
|
||
const message = inputMessage.value.trim();
|
||
if (!message || !isConnected.value || sending.value) {
|
||
return;
|
||
}
|
||
const lastMessage = messages.value[messages.value.length - 1];
|
||
if (lastMessage && lastMessage.type === "assistant" && lastMessage.loading) {
|
||
lastMessage.loading = false;
|
||
}
|
||
addMessage("user", message);
|
||
inputMessage.value = "";
|
||
const loadingMessage = {
|
||
id: generateId(),
|
||
type: "assistant",
|
||
content: "",
|
||
timestamp: Date.now(),
|
||
loading: true
|
||
};
|
||
messages.value.push(loadingMessage);
|
||
sending.value = true;
|
||
scrollToBottom();
|
||
try {
|
||
if ((ws == null ? void 0 : ws.readyState) === WebSocket.OPEN) {
|
||
ws.send(message);
|
||
} else {
|
||
throw new Error("WebSocket 连接未建立");
|
||
}
|
||
} catch (err) {
|
||
console.error("发送消息失败:", err);
|
||
messages.value.pop();
|
||
error.value = "发送消息失败,请检查连接状态";
|
||
ElMessage.error("发送失败");
|
||
} finally {
|
||
sending.value = false;
|
||
}
|
||
};
|
||
const addMessage = (type, content) => {
|
||
const message = {
|
||
id: generateId(),
|
||
type,
|
||
content,
|
||
timestamp: Date.now(),
|
||
// 长消息自动折叠
|
||
collapsed: content.length > 200
|
||
};
|
||
messages.value.push(message);
|
||
nextTick(() => scrollToBottom());
|
||
};
|
||
const clearCurrentChat = async () => {
|
||
try {
|
||
await ElMessageBox.confirm("确定要清空当前对话吗?此操作不可恢复。", "确认清空", {
|
||
confirmButtonText: "确定",
|
||
cancelButtonText: "取消",
|
||
type: "warning"
|
||
});
|
||
messages.value = [];
|
||
ElMessage.success("对话已清空");
|
||
} catch {
|
||
}
|
||
};
|
||
const setPrompt = (prompt) => {
|
||
inputMessage.value = prompt;
|
||
};
|
||
const copyMessage = async (content) => {
|
||
try {
|
||
await navigator.clipboard.writeText(content);
|
||
ElMessage.success("已复制到剪贴板");
|
||
} catch {
|
||
const textArea = document.createElement("textarea");
|
||
textArea.value = content;
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
document.execCommand("copy");
|
||
document.body.removeChild(textArea);
|
||
ElMessage.success("已复制到剪贴板");
|
||
}
|
||
};
|
||
const toggleMessageFold = (message) => {
|
||
message.collapsed = !message.collapsed;
|
||
};
|
||
const scrollToBottom = () => {
|
||
nextTick(() => {
|
||
if (messagesContainer.value) {
|
||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||
}
|
||
});
|
||
};
|
||
const formatMessage = (content) => {
|
||
if (!content) return "";
|
||
return md.render(content);
|
||
};
|
||
const generateId = () => {
|
||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||
};
|
||
onMounted(() => {
|
||
connectWebSocket();
|
||
});
|
||
onUnmounted(() => {
|
||
disconnectWebSocket();
|
||
});
|
||
return (_ctx, _cache) => {
|
||
const _component_el_icon = ElIcon;
|
||
const _component_el_button = ElButton;
|
||
const _component_el_alert = ElAlert;
|
||
const _component_el_input = ElInput;
|
||
return openBlock(), createElementBlock("div", _hoisted_1, [
|
||
createBaseVNode("div", _hoisted_2, [
|
||
createBaseVNode("div", _hoisted_3, [
|
||
_cache[8] || (_cache[8] = createBaseVNode("div", { class: "navbar-left" }, [
|
||
createBaseVNode("h2", null, "FA智能助手")
|
||
], -1)),
|
||
createBaseVNode("div", _hoisted_4, [
|
||
createBaseVNode("div", _hoisted_5, [
|
||
createVNode(_component_el_icon, {
|
||
class: normalizeClass(["status-icon", connectionStatus.value])
|
||
}, {
|
||
default: withCtx(() => [
|
||
connectionStatus.value === "connected" ? (openBlock(), createBlock(unref(connection_default), { key: 0 })) : connectionStatus.value === "connecting" ? (openBlock(), createBlock(unref(loading_default), { key: 1 })) : (openBlock(), createBlock(unref(warning_default), { key: 2 }))
|
||
]),
|
||
_: 1
|
||
}, 8, ["class"]),
|
||
createBaseVNode("span", _hoisted_6, toDisplayString(connectionStatusText.value), 1)
|
||
]),
|
||
messages.value.length > 0 ? (openBlock(), createBlock(_component_el_button, {
|
||
key: 0,
|
||
text: "",
|
||
icon: unref(delete_default),
|
||
onClick: clearCurrentChat
|
||
}, {
|
||
default: withCtx(() => [..._cache[7] || (_cache[7] = [
|
||
createTextVNode(" 清空对话 ", -1)
|
||
])]),
|
||
_: 1
|
||
}, 8, ["icon"])) : createCommentVNode("", true),
|
||
createVNode(_component_el_button, {
|
||
text: "",
|
||
icon: unref(setting_default),
|
||
onClick: toggleConnection
|
||
}, {
|
||
default: withCtx(() => [
|
||
createTextVNode(toDisplayString(isConnected.value ? "断开连接" : "重新连接"), 1)
|
||
]),
|
||
_: 1
|
||
}, 8, ["icon"])
|
||
])
|
||
]),
|
||
createBaseVNode("div", {
|
||
ref_key: "messagesContainer",
|
||
ref: messagesContainer,
|
||
class: "chat-messages"
|
||
}, [
|
||
messages.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_7, [
|
||
createBaseVNode("div", _hoisted_8, [
|
||
createBaseVNode("div", _hoisted_9, [
|
||
createVNode(_component_el_icon, { size: "64" }, {
|
||
default: withCtx(() => [
|
||
createVNode(unref(chat_dot_round_default))
|
||
]),
|
||
_: 1
|
||
})
|
||
]),
|
||
_cache[13] || (_cache[13] = createBaseVNode("h1", null, "FA智能助手", -1)),
|
||
_cache[14] || (_cache[14] = createBaseVNode("p", { class: "welcome-subtitle" }, " 我是您的专属AI助手,可以帮您回答问题、处理任务和进行智能对话 ", -1)),
|
||
createBaseVNode("div", _hoisted_10, [
|
||
createBaseVNode("div", {
|
||
class: "prompt-card",
|
||
onClick: _cache[0] || (_cache[0] = ($event) => setPrompt("请介绍一下FastApiAdmin系统"))
|
||
}, [..._cache[9] || (_cache[9] = [
|
||
createBaseVNode("h4", null, "系统介绍", -1),
|
||
createBaseVNode("p", null, "请介绍一下FastApiAdmin系统", -1)
|
||
])]),
|
||
createBaseVNode("div", {
|
||
class: "prompt-card",
|
||
onClick: _cache[1] || (_cache[1] = ($event) => setPrompt("如何在系统中创建新的模块?"))
|
||
}, [..._cache[10] || (_cache[10] = [
|
||
createBaseVNode("h4", null, "开发指导", -1),
|
||
createBaseVNode("p", null, "如何在系统中创建新的模块?", -1)
|
||
])]),
|
||
createBaseVNode("div", {
|
||
class: "prompt-card",
|
||
onClick: _cache[2] || (_cache[2] = ($event) => setPrompt("系统的权限管理是如何工作的?"))
|
||
}, [..._cache[11] || (_cache[11] = [
|
||
createBaseVNode("h4", null, "权限管理", -1),
|
||
createBaseVNode("p", null, "FA系统的权限管理是如何工作的?", -1)
|
||
])]),
|
||
createBaseVNode("div", {
|
||
class: "prompt-card",
|
||
onClick: _cache[3] || (_cache[3] = ($event) => setPrompt("如何优化FA系统的性能?"))
|
||
}, [..._cache[12] || (_cache[12] = [
|
||
createBaseVNode("h4", null, "性能优化", -1),
|
||
createBaseVNode("p", null, "如何优化系统的性能?", -1)
|
||
])])
|
||
])
|
||
])
|
||
])) : (openBlock(), createElementBlock("div", _hoisted_11, [
|
||
(openBlock(true), createElementBlock(Fragment, null, renderList(messages.value, (message) => {
|
||
return openBlock(), createElementBlock("div", {
|
||
key: message.id,
|
||
class: normalizeClass(["message-group", message.type])
|
||
}, [
|
||
createBaseVNode("div", _hoisted_12, [
|
||
message.type === "user" ? (openBlock(), createElementBlock("div", _hoisted_13, [
|
||
createVNode(_component_el_icon, null, {
|
||
default: withCtx(() => [
|
||
createVNode(unref(user_default))
|
||
]),
|
||
_: 1
|
||
})
|
||
])) : (openBlock(), createElementBlock("div", _hoisted_14, [
|
||
createVNode(_component_el_icon, null, {
|
||
default: withCtx(() => [
|
||
createVNode(unref(chat_dot_round_default))
|
||
]),
|
||
_: 1
|
||
})
|
||
]))
|
||
]),
|
||
createBaseVNode("div", _hoisted_15, [
|
||
createBaseVNode("div", _hoisted_16, [
|
||
createBaseVNode("strong", _hoisted_17, toDisplayString(message.type === "user" ? "You" : "FA助手"), 1)
|
||
]),
|
||
createBaseVNode("div", _hoisted_18, [
|
||
message.content.length > 200 ? (openBlock(), createBlock(_component_el_button, {
|
||
key: 0,
|
||
text: "",
|
||
size: "small",
|
||
icon: message.collapsed ? unref(arrow_down_default) : unref(arrow_up_default),
|
||
class: "fold-button",
|
||
onClick: ($event) => toggleMessageFold(message)
|
||
}, {
|
||
default: withCtx(() => [
|
||
createTextVNode(toDisplayString(message.collapsed ? "展开" : "收起"), 1)
|
||
]),
|
||
_: 2
|
||
}, 1032, ["icon", "onClick"])) : createCommentVNode("", true),
|
||
createBaseVNode("div", {
|
||
class: normalizeClass(["message-text", { collapsed: message.collapsed }]),
|
||
innerHTML: formatMessage(message.content)
|
||
}, null, 10, _hoisted_19),
|
||
message.type === "assistant" && message.loading && !message.content ? (openBlock(), createElementBlock("div", _hoisted_20, [..._cache[15] || (_cache[15] = [
|
||
createBaseVNode("div", { class: "typing-dots" }, [
|
||
createBaseVNode("span"),
|
||
createBaseVNode("span"),
|
||
createBaseVNode("span")
|
||
], -1)
|
||
])])) : createCommentVNode("", true)
|
||
]),
|
||
!message.loading ? (openBlock(), createElementBlock("div", _hoisted_21, [
|
||
createVNode(_component_el_button, {
|
||
text: "",
|
||
size: "small",
|
||
icon: unref(copy_document_default),
|
||
onClick: ($event) => copyMessage(message.content)
|
||
}, null, 8, ["icon", "onClick"]),
|
||
message.type === "assistant" ? (openBlock(), createBlock(_component_el_button, {
|
||
key: 0,
|
||
text: "",
|
||
size: "small",
|
||
icon: unref(refresh_left_default)
|
||
}, null, 8, ["icon"])) : createCommentVNode("", true)
|
||
])) : createCommentVNode("", true)
|
||
])
|
||
], 2);
|
||
}), 128))
|
||
])),
|
||
error.value ? (openBlock(), createElementBlock("div", _hoisted_22, [
|
||
createVNode(_component_el_alert, {
|
||
title: error.value,
|
||
type: "error",
|
||
closable: true,
|
||
"show-icon": "",
|
||
onClose: _cache[4] || (_cache[4] = ($event) => error.value = "")
|
||
}, null, 8, ["title"])
|
||
])) : createCommentVNode("", true)
|
||
], 512),
|
||
createBaseVNode("div", _hoisted_23, [
|
||
createBaseVNode("div", _hoisted_24, [
|
||
createBaseVNode("div", _hoisted_25, [
|
||
createVNode(_component_el_input, {
|
||
modelValue: inputMessage.value,
|
||
"onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => inputMessage.value = $event),
|
||
placeholder: isConnected.value ? "向FA助手发送消息..." : "请先连接到服务器",
|
||
disabled: !isConnected.value || sending.value,
|
||
type: "textarea",
|
||
rows: 1,
|
||
autosize: { minRows: 1, maxRows: 6 },
|
||
resize: "none",
|
||
class: "message-input",
|
||
onKeydown: [
|
||
withKeys(withModifiers(sendMessage, ["exact", "prevent"]), ["enter"]),
|
||
_cache[6] || (_cache[6] = withKeys(withModifiers(($event) => inputMessage.value += "\n", ["shift", "exact"]), ["enter"]))
|
||
]
|
||
}, null, 8, ["modelValue", "placeholder", "disabled", "onKeydown"]),
|
||
createVNode(_component_el_button, {
|
||
disabled: !inputMessage.value.trim() || !isConnected.value || sending.value,
|
||
loading: sending.value,
|
||
class: "send-button",
|
||
type: "primary",
|
||
circle: "",
|
||
onClick: sendMessage
|
||
}, {
|
||
default: withCtx(() => [
|
||
createVNode(_component_el_icon, null, {
|
||
default: withCtx(() => [
|
||
createVNode(unref(promotion_default))
|
||
]),
|
||
_: 1
|
||
})
|
||
]),
|
||
_: 1
|
||
}, 8, ["disabled", "loading"])
|
||
]),
|
||
_cache[16] || (_cache[16] = createBaseVNode("div", { class: "input-footer" }, [
|
||
createBaseVNode("span", { class: "input-hint" }, "按 Enter 发送消息,Shift + Enter 换行")
|
||
], -1))
|
||
])
|
||
])
|
||
])
|
||
]);
|
||
};
|
||
}
|
||
});
|
||
const index = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-9253047d"]]);
|
||
export {
|
||
index as default
|
||
};
|