🎨 优化正则模块和前端渲染功能

Signed-off-by: Echo <1711788888@qq.com>
This commit is contained in:
2026-03-03 03:40:03 +08:00
parent 3f8220340e
commit cbb4034a91
13 changed files with 613 additions and 95 deletions

View File

@@ -63,7 +63,7 @@ export interface UpdateCharacterRequest {
postHistoryInstructions?: string
tags?: string[]
alternateGreetings?: string[]
characterBook?: Record<string, any>
characterBook?: Record<string, any> | null
extensions?: Record<string, any>
isPublic?: boolean
}

View File

@@ -75,6 +75,24 @@ export default function ChatArea({ conversation, character, onConversationUpdate
scrollToBottom()
}, [messages])
// 监听状态栏按钮点击事件
useEffect(() => {
const handleStatusBarAction = (event: CustomEvent) => {
const action = event.detail
if (action && typeof action === 'string' && !sending) {
console.log('[ChatArea] 收到状态栏操作,自动发送消息:', action)
setInputValue(action)
// 延迟发送,确保 inputValue 已更新
setTimeout(() => {
handleSendMessage(action)
}, 100)
}
}
window.addEventListener('sendMessageFromStatusBar', handleStatusBarAction as EventListener)
return () => window.removeEventListener('sendMessageFromStatusBar', handleStatusBarAction as EventListener)
}, [sending])
const loadMessages = async () => {
try {
setLoading(true)
@@ -185,11 +203,10 @@ export default function ChatArea({ conversation, character, onConversationUpdate
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
const handleSend = async () => {
if (!inputValue.trim() || sending) return
const handleSendMessage = async (message: string) => {
if (!message.trim() || sending) return
const userMessage = inputValue.trim()
setInputValue('')
const userMessage = message.trim()
setSending(true)
const tempUserMessage: Message = {
@@ -277,6 +294,13 @@ export default function ChatArea({ conversation, character, onConversationUpdate
}
}
const handleSend = async () => {
if (!inputValue.trim() || sending) return
const userMessage = inputValue.trim()
setInputValue('')
await handleSendMessage(userMessage)
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey && !sending && inputValue.trim()) {
e.preventDefault()
@@ -644,7 +668,7 @@ export default function ChatArea({ conversation, character, onConversationUpdate
>
<MessageContent
content={msg.content}
role={msg.role}
role={msg.role as 'user' | 'assistant'}
onChoiceSelect={(choice) => {
setInputValue(choice)
textareaRef.current?.focus()

View File

@@ -127,21 +127,35 @@ function parseChoices(content: string): { choices: Choice[]; cleanContent: strin
return { choices, cleanContent }
}
// 清理脚本输出内容
function cleanScriptOutput(content: string): string {
// 移除 <UpdateVariable>...</UpdateVariable> 块
let cleaned = content.replace(/<UpdateVariable>[\s\S]*?<\/UpdateVariable>/gi, '')
// 提取 <maintext> 内容
function extractMaintext(content: string): { maintext: string; cleanContent: string } {
const maintextRegex = /<maintext>([\s\S]*?)<\/maintext>/i
const match = content.match(maintextRegex)
// 移除 <Analysis>...</Analysis> 块
cleaned = cleaned.replace(/<Analysis>[\s\S]*?<\/Analysis>/gi, '')
if (!match) {
return { maintext: '', cleanContent: content }
}
// 移除 _.set() 调用
cleaned = cleaned.replace(/^\s*_.set\([^)]+\);\s*$/gm, '')
return cleaned.trim()
const maintext = match[1].trim()
const cleanContent = content.replace(maintextRegex, '').trim()
return { maintext, cleanContent }
}
// 解析状态面板数据
// 提取 <Status_block> YAML 数据
function extractStatusBlock(content: string): { statusYaml: string; cleanContent: string } {
const statusRegex = /<Status_block>\s*([\s\S]*?)\s*<\/Status_block>/i
const match = content.match(statusRegex)
if (!match) {
return { statusYaml: '', cleanContent: content }
}
const statusYaml = match[1].trim()
const cleanContent = content.replace(statusRegex, '').trim()
return { statusYaml, cleanContent }
}
// 解析状态面板数据JSON 格式 - 保留兼容性)
function parseStatusPanel(content: string): { status: any; cleanContent: string } {
const statusRegex = /<status_current_variable>([\s\S]*?)<\/status_current_variable>/i
const match = content.match(statusRegex)
@@ -167,28 +181,49 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
const [allowScript, setAllowScript] = useState(false)
const [choices, setChoices] = useState<Choice[]>([])
const [displayContent, setDisplayContent] = useState(content)
const [remainingText, setRemainingText] = useState('')
const [statusPanel, setStatusPanel] = useState<any>(null)
const [statusYaml, setStatusYaml] = useState<string>('')
const [maintext, setMaintext] = useState<string>('')
const iframeRef = useRef<HTMLIFrameElement>(null)
const statusIframeRef = useRef<HTMLIFrameElement>(null)
useEffect(() => {
console.log('[MessageContent] 原始内容:', content)
let processedContent = content
// 解析状态面板
const { status, cleanContent: contentAfterStatus } = parseStatusPanel(processedContent)
console.log('[MessageContent] 状态面板:', status)
setStatusPanel(status)
// 提取 <maintext> 内容
const { maintext: extractedMaintext, cleanContent: contentAfterMaintext } = extractMaintext(processedContent)
if (extractedMaintext) {
console.log('[MessageContent] 提取到 maintext:', extractedMaintext)
setMaintext(extractedMaintext)
processedContent = contentAfterMaintext
}
// 提取 <Status_block> YAML 数据
const { statusYaml: extractedYaml, cleanContent: contentAfterStatus } = extractStatusBlock(processedContent)
if (extractedYaml) {
console.log('[MessageContent] 提取到 Status_block YAML:', extractedYaml)
setStatusYaml(extractedYaml)
processedContent = contentAfterStatus
}
// 解析状态面板JSON 格式 - 保留兼容性)
const { status, cleanContent: contentAfterStatusPanel } = parseStatusPanel(processedContent)
if (status) {
console.log('[MessageContent] 状态面板:', status)
setStatusPanel(status)
processedContent = contentAfterStatusPanel
}
// 解析选择项
const { choices: parsedChoices, cleanContent } = parseChoices(contentAfterStatus)
setChoices(parsedChoices)
console.log('[MessageContent] 选择项:', parsedChoices)
// 清理脚本输出
const finalContent = cleanScriptOutput(cleanContent)
console.log('[MessageContent] 清理后内容:', finalContent)
// 直接使用清理后的内容,不再进行脚本输出清理
const finalContent = cleanContent
console.log('[MessageContent] 最终内容:', finalContent)
// 检测是否包含 HTML 标签或代码块
const htmlRegex = /<[^>]+>/g
@@ -277,7 +312,6 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
}
setDisplayContent(renderedContent)
setRemainingText('')
setHasHtml(hasHtmlTags || hasCodeBlocks)
setHasScript(hasScriptContent)
@@ -336,6 +370,30 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
max-width: 100% !important;
}
</style>
<script>
// 监听来自父页面的消息(用于更新状态栏)
window.addEventListener('message', function(event) {
if (event.data.type === 'updateStatus') {
// 更新状态栏中的变量值
Object.keys(event.data.variables || {}).forEach(function(key) {
var elements = document.querySelectorAll('[data-var="' + key + '"]');
elements.forEach(function(el) {
el.textContent = event.data.variables[key];
});
});
}
});
// 向父页面发送消息的辅助函数
function sendToParent(type, data) {
window.parent.postMessage({ type: type, data: data }, '*');
}
// 用户操作触发函数(供状态栏按钮调用)
function onPlayerAction(action) {
sendToParent('playerAction', { action: action });
}
</script>
</head>
<body>
${displayContent}
@@ -354,6 +412,239 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
}
}
// 渲染状态栏 iframe使用 doc.write 动态注入 YAML 数据)
const renderStatusBar = () => {
if (!statusIframeRef.current || !statusYaml) return
console.log('[MessageContent] 开始渲染状态栏YAML 数据长度:', statusYaml.length)
const iframe = statusIframeRef.current
const doc = iframe.contentDocument || iframe.contentWindow?.document
if (doc) {
// 使用 doc.write() 分步注入,避免模板字符串的转义问题
doc.open()
// 1. 写入 HTML 头部
doc.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"><\/script>
<script src="https://cdn.tailwindcss.com"><\/script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
body {
margin: 0;
padding: 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: transparent;
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
}
.status-block {
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
padding: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.status-title {
font-size: 16px;
font-weight: 600;
color: rgba(157, 124, 245, 1);
}
.status-info {
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
margin: 4px 0;
}
.character-card {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 12px;
margin: 8px 0;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.character-name {
font-weight: 600;
font-size: 15px;
margin-bottom: 8px;
color: rgba(157, 124, 245, 1);
}
.attribute {
margin: 4px 0;
font-size: 13px;
line-height: 1.5;
}
.attribute-key {
color: rgba(157, 124, 245, 0.8);
font-weight: 500;
}
.option-item {
padding: 8px 12px;
margin: 4px 0;
background: rgba(157, 124, 245, 0.1);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
border-left: 2px solid rgba(157, 124, 245, 0.3);
}
.option-item:hover {
background: rgba(157, 124, 245, 0.2);
border-left-color: rgba(157, 124, 245, 0.7);
}
</style>
</head>
<body>
`)
// 2. 创建 YAML script 标签并直接设置 textContent避免 HTML 转义)
doc.write('<script id="yaml-data-source" type="text/yaml"><\/script>')
// 3. 写入渲染脚本
doc.write(`
<script>
// 向父页面发送消息
function sendToParent(type, data) {
window.parent.postMessage({ type: type, data: data }, '*');
}
// 用户操作触发函数
function onPlayerAction(action) {
console.log('[StatusBar] 发送操作:', action);
sendToParent('playerAction', { action: action });
}
// 立即执行渲染
(function() {
try {
const yamlScript = document.getElementById('yaml-data-source');
if (!yamlScript || !yamlScript.textContent.trim()) {
document.body.innerHTML = '<div style="padding:20px;text-align:center;color:rgba(255,255,255,0.5);">状态栏数据加载中...</div>';
return;
}
const yamlData = jsyaml.load(yamlScript.textContent);
if (!yamlData || Object.keys(yamlData).length === 0) {
document.body.innerHTML = '<div style="padding:20px;text-align:center;color:rgba(255,255,255,0.5);">状态栏数据为空</div>';
return;
}
// 渲染状态栏
const rootKey = Object.keys(yamlData)[0];
const data = yamlData[rootKey];
let html = '<div class="status-block">';
// 渲染标题
html += '<div class="status-header">';
html += '<div class="status-title">' + rootKey + '</div>';
html += '</div>';
// 渲染基本信息(日期、地点等)
Object.entries(data).forEach(([key, value]) => {
if (key.includes('日期') || key.includes('时间') || key.includes('地点') || key.includes('位置')) {
html += '<div class="status-info">' + value + '</div>';
}
});
// 渲染用户列表
if (data['用户列表'] && Array.isArray(data['用户列表'])) {
html += '<div style="margin-top:12px;">';
data['用户列表'].forEach(userItem => {
const userData = userItem['用户'] || userItem;
if (typeof userData === 'object') {
html += '<div class="character-card">';
const userName = userData['名字'] || '未知角色';
html += '<div class="character-name">' + userName + '</div>';
Object.entries(userData).forEach(([k, v]) => {
if (k !== '名字') {
if (Array.isArray(v)) {
html += '<div class="attribute"><span class="attribute-key">' + k + ':</span></div>';
v.forEach(item => {
if (typeof item === 'object') {
Object.values(item).forEach(val => {
html += '<div style="margin-left:12px;font-size:12px;">' + val + '</div>';
});
} else {
html += '<div style="margin-left:12px;font-size:12px;">' + item + '</div>';
}
});
} else {
html += '<div class="attribute"><span class="attribute-key">' + k + ':</span> ' + v + '</div>';
}
}
});
html += '</div>';
}
});
html += '</div>';
}
// 渲染行动选项
if (data['行动选项'] && typeof data['行动选项'] === 'object') {
const actionOptions = data['行动选项'];
html += '<div style="margin-top:12px;">';
html += '<div style="font-weight:600;margin-bottom:8px;color:rgba(157,124,245,1);">';
html += (actionOptions['名字'] || '角色') + ' 的行动选项';
html += '</div>';
if (actionOptions['选项'] && Array.isArray(actionOptions['选项'])) {
actionOptions['选项'].forEach(option => {
const escapedOption = option.replace(/'/g, "\\\\'").replace(/"/g, '&quot;');
html += '<div class="option-item" onclick="onPlayerAction(\\'' + escapedOption + '\\')">' + option + '</div>';
});
}
html += '</div>';
}
html += '</div>';
document.body.innerHTML = html;
console.log('[StatusBar] 渲染成功');
} catch (error) {
console.error('[StatusBar] 渲染失败:', error);
document.body.innerHTML = '<div style="padding:20px;text-align:center;color:rgba(255,100,100,0.8);">状态栏渲染失败: ' + error.message + '</div>';
}
})();
<\/script>
</body>
</html>
`)
doc.close()
// 4. 关键步骤:直接设置 YAML 数据到 script 标签的 textContent
// 这样可以避免 HTML 转义问题
const yamlScript = doc.getElementById('yaml-data-source')
if (yamlScript) {
yamlScript.textContent = statusYaml
console.log('[MessageContent] YAML 数据已注入,长度:', statusYaml.length)
} else {
console.error('[MessageContent] 找不到 yaml-data-source 元素')
}
// 5. 自动调整 iframe 高度
setTimeout(() => {
if (doc.body) {
const height = doc.body.scrollHeight
iframe.style.height = `${Math.max(height + 32, 200)}px`
console.log('[MessageContent] iframe 高度已调整:', iframe.style.height)
}
}, 300)
}
}
useEffect(() => {
if (allowScript && hasScript && iframeRef.current) {
// 延迟渲染确保 iframe 已挂载
@@ -363,6 +654,15 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
}
}, [allowScript, hasScript, displayContent])
useEffect(() => {
if (statusYaml && statusIframeRef.current) {
// 延迟渲染确保 iframe 已挂载
setTimeout(() => {
renderStatusBar()
}, 50)
}
}, [statusYaml])
// 如果是用户消息,直接显示纯文本
if (role === 'user') {
return <p className="text-sm leading-relaxed whitespace-pre-wrap break-words overflow-wrap-anywhere">{content}</p>
@@ -451,7 +751,7 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
</div>
)}
{/* 状态面板 */}
{/* 状态面板JSON 格式 - 保留兼容性) */}
{statusPanel && (
<div className="mt-4 p-4 glass rounded-lg border border-secondary/20">
<div className="text-sm font-medium text-secondary mb-3"></div>
@@ -478,6 +778,31 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
</div>
</div>
)}
{/* 状态栏YAML 格式 - SillyTavern 兼容) */}
{statusYaml && (
<div className="mt-4 w-full border border-white/10 rounded-lg bg-black/20 overflow-hidden">
<iframe
ref={statusIframeRef}
className="w-full"
sandbox="allow-scripts allow-same-origin"
style={{ minHeight: '200px', border: 'none' }}
/>
</div>
)}
{/* maintext 内容(如果提取到) */}
{maintext && !statusYaml && (
<div className="mt-4 p-4 glass rounded-lg border border-primary/20">
<div className="text-sm leading-relaxed break-words overflow-wrap-anywhere">
{maintext.split('\n').map((line, index) => (
<div key={index} className="min-h-[1.5em]">
{line ? <DialogueText text={line} /> : <br />}
</div>
))}
</div>
</div>
)}
</div>
)
}

View File

@@ -124,7 +124,7 @@ export default function AdminPage() {
function AIConfigSection() {
const [configs, setConfigs] = useState<AIConfig[]>([])
const [loading, setLoading] = useState(true)
const [showAddModal, setShowAddModal] = useState(false)
const [_showAddModal, setShowAddModal] = useState(false)
const [editingConfig, setEditingConfig] = useState<AIConfig | null>(null)
const [testingConfig, setTestingConfig] = useState<number | null>(null)
const [testResult, setTestResult] = useState<{ success: boolean; message: string; latency: number } | null>(null)
@@ -286,7 +286,7 @@ function AIConfigSection() {
</div>
)}
{(showAddModal || editingConfig) && (
{(_showAddModal || editingConfig) && (
<AIConfigModal
config={editingConfig}
onClose={() => {
@@ -800,8 +800,8 @@ function CharactersManageSection() {
function PresetsManageSection() {
const [presets, setPresets] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [showAddModal, setShowAddModal] = useState(false)
const [editingPreset, setEditingPreset] = useState<any>(null)
const [_showAddModal, setShowAddModal] = useState(false)
const [_editingPreset, setEditingPreset] = useState<any>(null)
useEffect(() => {
loadPresets()
@@ -971,8 +971,8 @@ function UsersManageSection() {
}
}
const handleToggleStatus = async (id: number, enable: boolean) => {
if (!confirm(`确定要${enable ? '禁用' : '启用'}这个用户吗?`)) return
const handleToggleStatus = async (_id: number, _enable: boolean) => {
if (!confirm(`确定要${_enable ? '禁用' : '启用'}这个用户吗?`)) return
try {
// 需要后端提供更新用户状态的接口
@@ -983,8 +983,8 @@ function UsersManageSection() {
}
}
const handleToggleAdmin = async (id: number, isAdmin: boolean) => {
if (!confirm(`确定要${isAdmin ? '移除' : '授予'}管理员权限吗?`)) return
const handleToggleAdmin = async (_id: number, _isAdmin: boolean) => {
if (!confirm(`确定要${_isAdmin ? '移除' : '授予'}管理员权限吗?`)) return
try {
// 需要后端提供更新用户权限的接口

View File

@@ -4,7 +4,7 @@ import Navbar from '../components/Navbar'
import {Book, Code2, Download, Edit, FileJson, FileUp, Image as ImageIcon, Plus, Search, Trash2, X} from 'lucide-react'
import {type Character, characterApi} from '../api/character'
import {type RegexScript, regexScriptApi} from '../api/regex'
import {useAppStore} from '../store'
// import {useAppStore} from '../store'
interface WorldBookEntry {
keys: string[]
@@ -36,7 +36,7 @@ export default function CharacterManagePage() {
const [worldBookEntries, setWorldBookEntries] = useState<WorldBookEntry[]>([])
const [showRegexScriptEditor, setShowRegexScriptEditor] = useState(false)
const [regexScripts, setRegexScripts] = useState<RegexScript[]>([])
const [editingTab, setEditingTab] = useState<'basic' | 'worldbook' | 'regex'>('basic')
const [_editingTab, _setEditingTab] = useState<'basic' | 'worldbook' | 'regex'>('basic')
const [showAddRegexModal, setShowAddRegexModal] = useState(false)
const [newRegexForm, setNewRegexForm] = useState({
name: '',
@@ -82,7 +82,7 @@ export default function CharacterManagePage() {
const loadRegexScripts = async (characterId: number) => {
try {
const response = await regexScriptApi.getRegexScriptList({
const response = await regexScriptApi.getList({
scope: 1,
pageSize: 100,
})
@@ -766,7 +766,7 @@ export default function CharacterManagePage() {
<button
onClick={async () => {
try {
await regexScriptApi.createRegexScript({
await regexScriptApi.create({
name: '新脚本',
findRegex: '.*', // 默认匹配所有内容
replaceWith: '',
@@ -821,7 +821,7 @@ export default function CharacterManagePage() {
onClick={async () => {
if (!confirm('确定要删除这个脚本吗?')) return
try {
await regexScriptApi.deleteRegexScript(script.id)
await regexScriptApi.delete(script.id)
loadRegexScripts(selectedCharacter.id)
} catch (err: any) {
alert(err.response?.data?.msg || '删除失败')
@@ -927,7 +927,7 @@ export default function CharacterManagePage() {
if (!testInput) return
try {
const response = await regexScriptApi.testRegexScript(script.id, testInput)
const response = await regexScriptApi.test(script.id, testInput)
console.log('测试响应:', response)
// 处理响应数据结构
@@ -949,7 +949,7 @@ export default function CharacterManagePage() {
<button
onClick={async () => {
try {
await regexScriptApi.updateRegexScript(script.id, {
await regexScriptApi.update(script.id, {
name: script.name,
findRegex: script.findRegex,
replaceWith: script.replaceWith,
@@ -1039,7 +1039,7 @@ export default function CharacterManagePage() {
const scripts = Array.isArray(jsonData) ? jsonData : [jsonData]
for (const scriptData of scripts) {
await regexScriptApi.createRegexScript({
await regexScriptApi.create({
name: scriptData.scriptName || scriptData.name || '导入的脚本',
findRegex: scriptData.findRegex || '.*',
replaceWith: scriptData.replaceString || scriptData.replaceWith || '',
@@ -1170,7 +1170,7 @@ export default function CharacterManagePage() {
}
try {
await regexScriptApi.createRegexScript({
await regexScriptApi.create({
name: newRegexForm.name,
findRegex: newRegexForm.findRegex,
replaceWith: newRegexForm.replaceWith,

View File

@@ -18,8 +18,7 @@ export default function ChatPage() {
// 使用 MVU store
const {
currentConversation,
currentCharacter,
currentConversation: storeConversation,
loading,
sidebarOpen: showSidebar,
setCurrentConversation,
@@ -29,6 +28,18 @@ export default function ChatPage() {
setVariable,
} = useAppStore()
// 使用本地状态来存储完整的 Conversation 和 Character 对象
const [currentConversation, setLocalConversation] = useState<Conversation | null>(null)
const [currentCharacter, setLocalCharacter] = useState<Character | null>(null)
// 同步 store 和本地状态
useEffect(() => {
if (storeConversation && currentConversation?.id !== storeConversation.id) {
// 当 store 中的对话改变时,重新加载完整数据
initializeChat()
}
}, [storeConversation?.id])
// 从URL参数获取角色ID或对话ID
const characterId = searchParams.get('character')
const conversationId = searchParams.get('conversation')
@@ -37,6 +48,23 @@ export default function ChatPage() {
initializeChat()
}, [characterId, conversationId])
// 监听 iframe 消息(状态栏按钮点击事件)
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data.type === 'playerAction') {
// 用户点击了状态栏中的按钮,自动发送消息
const action = event.data.data.action
console.log('[ChatPage] 收到状态栏操作:', action)
// 这里需要触发消息发送,但需要访问 ChatArea 的 sendMessage 方法
// 暂时通过自定义事件来实现
window.dispatchEvent(new CustomEvent('sendMessageFromStatusBar', { detail: action }))
}
}
window.addEventListener('message', handleMessage)
return () => window.removeEventListener('message', handleMessage)
}, [])
useEffect(() => {
if (currentConversation?.settings) {
try {
@@ -72,10 +100,12 @@ export default function ChatPage() {
// 如果有对话ID加载现有对话
if (conversationId) {
const convResp = await conversationApi.getConversationById(Number(conversationId))
setLocalConversation(convResp.data)
setCurrentConversation(convResp.data)
// 加载对话关联的角色
const charResp = await characterApi.getCharacterById(convResp.data.characterId)
setLocalCharacter(charResp.data)
setCurrentCharacter(charResp.data)
// 更新变量系统
@@ -84,6 +114,7 @@ export default function ChatPage() {
// 如果有角色ID创建新对话
else if (characterId) {
const charResp = await characterApi.getCharacterById(Number(characterId))
setLocalCharacter(charResp.data)
setCurrentCharacter(charResp.data)
// 更新变量系统
@@ -94,6 +125,7 @@ export default function ChatPage() {
characterId: Number(characterId),
title: `${charResp.data.name} 的对话`,
})
setLocalConversation(convResp.data)
setCurrentConversation(convResp.data)
// 更新URL为对话ID
@@ -108,10 +140,16 @@ export default function ChatPage() {
}
const handleConversationChange = (conversation: Conversation) => {
setLocalConversation(conversation)
setCurrentConversation(conversation)
navigate(`/chat?conversation=${conversation.id}`)
}
const handleConversationUpdate = (conversation: Conversation) => {
setLocalConversation(conversation)
setCurrentConversation(conversation)
}
const handleSettingsUpdate = (settings: any) => {
if (settings.backgroundImage !== undefined) {
setBackgroundImage(settings.backgroundImage)
@@ -188,7 +226,7 @@ export default function ChatPage() {
<ChatArea
conversation={currentConversation}
character={currentCharacter}
onConversationUpdate={setCurrentConversation}
onConversationUpdate={handleConversationUpdate}
/>
{showCharacterPanel && (
<CharacterPanel

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'
import { Plus, Search, Edit2, Trash2, Upload, Download, Play, X } from 'lucide-react'
import { Plus, Search, Edit2, Trash2, Upload, Download, X } from 'lucide-react'
import { regexAPI } from '../api/regex'
interface RegexScript {

View File

@@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
import {
getWorldbookList,
createWorldbook,
updateWorldbook,
deleteWorldbook,
importWorldbook,
exportWorldbook,
@@ -21,7 +20,7 @@ const WorldbookManagePage: React.FC = () => {
const [selectedWorldbook, setSelectedWorldbook] = useState<Worldbook | null>(null);
const [entries, setEntries] = useState<WorldbookEntry[]>([]);
const [selectedEntry, setSelectedEntry] = useState<WorldbookEntry | null>(null);
const [loading, setLoading] = useState(false);
const [_loading, setLoading] = useState(false);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEntryModal, setShowEntryModal] = useState(false);
const [searchKeyword, setSearchKeyword] = useState('');

9
web-app/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE_URL?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@@ -3,4 +3,20 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
// 禁用文件名哈希,强制浏览器重新加载
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
}
},
server: {
// 开发服务器禁用缓存
headers: {
'Cache-Control': 'no-store',
}
}
})