前端修饰与渲染处理

This commit is contained in:
2026-04-26 03:34:47 +08:00
parent 35eff3faf6
commit 6b5ddac178
114 changed files with 18465 additions and 132 deletions

7
client/.npmrc Normal file
View File

@@ -0,0 +1,7 @@
registry=https://registry.npmmirror.com
fetch-timeout=600000
maxsockets=10
prefer-offline=true
audit=false
fund=false
loglevel=error

7
client/.npmrc.tencent Normal file
View File

@@ -0,0 +1,7 @@
registry=https://mirrors.cloud.tencent.com/npm/
fetch-timeout=600000
maxsockets=10
prefer-offline=true
audit=false
fund=false
loglevel=error

13
client/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/src/assets/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SillyTavern Repalice</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
@@ -25,6 +25,6 @@
"eslint-plugin-vue": "^9.19.2",
"typescript": "^5.3.3",
"vite": "^5.0.11",
"vue-tsc": "^1.8.27"
"vue-tsc": "^2.0.0"
}
}

View File

@@ -3,7 +3,16 @@
</template>
<script setup lang="ts">
// Root component
import { onMounted } from 'vue';
import { useTheme } from '@/composables/useTheme';
// Initialize theme
const { theme } = useTheme();
onMounted(() => {
// Apply initial theme
document.documentElement.setAttribute('data-theme', theme.value);
});
</script>
<style>

View File

@@ -1,17 +0,0 @@
<template>
<div class="center-panel">
<p>Center Panel - Chat Interface</p>
</div>
</template>
<script setup lang="ts">
// CenterPanel component placeholder
</script>
<style scoped>
.center-panel {
flex: 1;
background-color: var(--color-bg-primary);
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1 @@
# This directory is deprecated. Files have been moved to features/ module structure.

View File

@@ -0,0 +1 @@
# This directory is deprecated. Files have been moved to features/ module structure.

View File

@@ -1,18 +0,0 @@
<template>
<div class="left-panel">
<p>Left Panel - Character List & Chat History</p>
</div>
</template>
<script setup lang="ts">
// LeftPanel component placeholder
</script>
<style scoped>
.left-panel {
width: 300px;
background-color: var(--color-bg-secondary);
border-right: 1px solid var(--color-border);
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1 @@
# This directory is deprecated. Files have been moved to features/ module structure.

View File

@@ -1,18 +0,0 @@
<template>
<div class="right-panel">
<p>Right Panel - Character Detail & Workflow Editor</p>
</div>
</template>
<script setup lang="ts">
// RightPanel component placeholder
</script>
<style scoped>
.right-panel {
width: 400px;
background-color: var(--color-bg-secondary);
border-left: 1px solid var(--color-border);
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,122 @@
<template>
<div class="dual-tab-container">
<div class="tab-header">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab-button"
:class="{ active: modelValue.includes(tab.id) }"
@click="handleTabClick(tab.id)"
>
<span class="tab-label">{{ tab.label }}</span>
</button>
</div>
<div class="tab-content">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
interface Tab {
id: string;
label: string;
}
const props = defineProps<{
tabs: Tab[];
modelValue: string[];
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void;
}>();
function handleTabClick(tabId: string) {
const isSelected = props.modelValue.includes(tabId);
if (isSelected) {
// 如果已选中,取消选择
emit('update:modelValue', props.modelValue.filter(id => id !== tabId));
} else {
// 如果未选中,添加到选择列表
const newSelection = [...props.modelValue, tabId];
// 限制最多选择2个
if (newSelection.length > 2) {
// 移除最早选中的(第一个)
newSelection.shift();
}
emit('update:modelValue', newSelection);
}
}
</script>
<style scoped>
.dual-tab-container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.tab-header {
display: flex;
gap: var(--spacing-xs);
padding: var(--spacing-md) var(--spacing-md) 0;
border-bottom: 1px solid var(--color-border-light);
background-color: var(--color-bg-secondary);
flex-shrink: 0;
}
.tab-button {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
border: none;
background: transparent;
color: var(--color-text-secondary);
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-normal);
position: relative;
border-radius: var(--radius-md) var(--radius-md) 0 0;
letter-spacing: -0.01em;
}
.tab-button::after {
content: '';
position: absolute;
bottom: -1px;
left: 50%;
transform: translateX(-50%) scaleX(0);
width: 60%;
height: 2px;
background: var(--gradient-primary);
transition: transform var(--transition-normal);
border-radius: var(--radius-full);
}
.tab-button:hover {
color: var(--color-text-primary);
background-color: var(--color-bg-tertiary);
}
.tab-button.active {
color: var(--color-accent);
background-color: var(--color-bg-tertiary);
}
.tab-button.active::after {
transform: translateX(-50%) scaleX(1);
}
.tab-content {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,102 @@
<template>
<div class="tab-container">
<div class="tab-header">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab-button"
:class="{ active: modelValue === tab.id }"
@click="$emit('update:modelValue', tab.id)"
>
<span class="tab-label">{{ tab.label }}</span>
</button>
</div>
<div class="tab-content">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
interface Tab {
id: string;
label: string;
}
defineProps<{
tabs: Tab[];
modelValue: string;
}>();
defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
</script>
<style scoped>
.tab-container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.tab-header {
display: flex;
gap: var(--spacing-xs);
padding: var(--spacing-md) var(--spacing-md) 0;
border-bottom: 1px solid var(--color-border-light);
background-color: var(--color-bg-secondary);
flex-shrink: 0;
}
.tab-button {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
border: none;
background: transparent;
color: var(--color-text-secondary);
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-normal);
position: relative;
border-radius: var(--radius-md) var(--radius-md) 0 0;
letter-spacing: -0.01em;
}
.tab-button::after {
content: '';
position: absolute;
bottom: -1px;
left: 50%;
transform: translateX(-50%) scaleX(0);
width: 60%;
height: 2px;
background: var(--gradient-primary);
transition: transform var(--transition-normal);
border-radius: var(--radius-full);
}
.tab-button:hover {
color: var(--color-text-primary);
background-color: var(--color-bg-tertiary);
}
.tab-button.active {
color: var(--color-accent);
background-color: var(--color-bg-tertiary);
}
.tab-button.active::after {
transform: translateX(-50%) scaleX(1);
}
.tab-content {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,2 @@
export { useTheme } from './useTheme';
export { useLocalStorage } from './useLocalStorage';

View File

@@ -0,0 +1,14 @@
import { ref, watch } from 'vue';
export function useLocalStorage<T>(key: string, initialValue: T) {
// Get value from localStorage or use initial value
const storedValue = localStorage.getItem(key);
const value = ref<T>(storedValue ? JSON.parse(storedValue) : initialValue);
// Watch for changes and update localStorage
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
});
return value;
}

View File

@@ -0,0 +1,24 @@
import { watch } from 'vue';
import { useAppStore } from '@/stores/useAppStore';
export function useTheme() {
const appStore = useAppStore();
function toggleTheme() {
appStore.toggleTheme();
}
// Watch theme changes and apply to document
watch(
() => appStore.theme,
(newTheme) => {
document.documentElement.setAttribute('data-theme', newTheme);
},
{ immediate: true }
);
return {
theme: appStore.theme,
toggleTheme,
};
}

7
client/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@@ -0,0 +1,51 @@
<template>
<div class="center-panel">
<MessageList
:render-markdown="chatInputRef?.renderMarkdown"
:render-html="chatInputRef?.renderHTML"
/>
<ChatInput ref="chatInputRef" />
</div>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue';
import MessageList from './features/MessageList/MessageList.vue';
import ChatInput from './features/ChatInput/ChatInput.vue';
// Use shallowRef to prevent Vue from unwrapping the nested refs
const chatInputRef = shallowRef<InstanceType<typeof ChatInput> | null>(null);
</script>
<style scoped>
.center-panel {
flex: 0 0 60%;
min-width: 0;
display: flex;
flex-direction: column;
background-color: var(--color-bg-primary);
overflow: hidden;
position: relative;
}
/* Subtle gradient background for visual interest */
.center-panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 30%, rgba(109, 140, 255, 0.04) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(109, 140, 255, 0.03) 0%, transparent 50%),
linear-gradient(180deg, var(--color-bg-primary) 0%, var(--color-bg-subtle) 100%);
pointer-events: none;
z-index: 0;
}
.center-panel > * {
position: relative;
z-index: 1;
}
</style>

View File

@@ -0,0 +1,3 @@
.center-panel {
/* Center panel base styles */
}

View File

@@ -0,0 +1 @@
# CenterPanel Features

View File

@@ -0,0 +1 @@
# ChatInput Feature

View File

@@ -0,0 +1,322 @@
<template>
<div class="chat-input">
<div class="input-container">
<div class="options-wrapper">
<button
class="options-toggle"
:class="{ active: showOptions }"
@click="showOptions = !showOptions"
title="Toggle Options"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
</button>
<transition name="slide-down">
<div v-if="showOptions" class="input-options">
<label class="option-checkbox">
<input type="checkbox" v-model="renderHTML" />
<span class="checkmark"></span>
<span class="option-label">HTML渲染</span>
</label>
<label class="option-checkbox">
<input type="checkbox" v-model="renderMarkdown" />
<span class="checkmark"></span>
<span class="option-label">MD渲染</span>
</label>
<label class="option-checkbox">
<input type="checkbox" v-model="enableDynamicTables" />
<span class="checkmark"></span>
<span class="option-label">动态表格</span>
</label>
<label class="option-checkbox">
<input type="checkbox" v-model="enableDrawing" />
<span class="checkmark"></span>
<span class="option-label">绘图</span>
</label>
</div>
</transition>
</div>
<textarea
placeholder="Type your message..."
class="message-input"
rows="1"
></textarea>
<button class="send-btn" title="Send Message">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useLocalStorage } from '@/composables/useLocalStorage';
const showOptions = ref(false);
// Render options with localStorage persistence
const renderMarkdown = useLocalStorage<boolean>('message-render-markdown', true);
const renderHTML = useLocalStorage<boolean>('message-render-html', true);
const enableDynamicTables = useLocalStorage<boolean>('message-render-tables', false);
const enableDrawing = useLocalStorage<boolean>('message-render-drawing', false);
// Debug: Watch for changes
watch(renderMarkdown, (newVal) => {
console.log('[ChatInput] renderMarkdown changed to:', newVal);
});
watch(renderHTML, (newVal) => {
console.log('[ChatInput] renderHTML changed to:', newVal);
});
// Expose render options for parent components to use
defineExpose({
renderMarkdown,
renderHTML,
enableDynamicTables,
enableDrawing
});
</script>
<style scoped>
.chat-input {
flex-shrink: 0;
padding: var(--spacing-md) var(--spacing-lg);
border-top: 1px solid var(--color-border-light);
background-color: var(--color-bg-secondary);
box-shadow: var(--shadow-lg), 0 -4px 12px rgba(0, 0, 0, 0.03);
width: 100%;
}
.input-container {
display: flex;
gap: var(--spacing-sm);
width: 100%;
align-items: flex-end;
}
.options-wrapper {
position: relative;
flex-shrink: 0;
}
.options-toggle {
width: 44px;
height: 44px;
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-bg-primary);
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
cursor: pointer;
transition: all var(--transition-normal);
box-shadow: var(--shadow-inner);
}
.options-toggle:hover {
background-color: var(--color-bg-secondary);
border-color: var(--color-accent);
color: var(--color-accent);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.options-toggle.active {
background-color: var(--color-accent-light);
border-color: var(--color-accent);
color: var(--color-accent);
}
.options-toggle svg {
transition: transform var(--transition-normal);
}
.options-toggle.active svg {
transform: rotate(90deg);
}
.input-options {
position: absolute;
bottom: calc(100% + var(--spacing-sm));
left: 0;
background-color: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--spacing-sm);
box-shadow: var(--shadow-xl);
z-index: var(--z-dropdown);
min-width: 140px;
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.option-checkbox {
display: flex;
align-items: center;
gap: var(--spacing-xs);
cursor: pointer;
position: relative;
user-select: none;
}
.option-checkbox input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
height: 16px;
width: 16px;
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
flex-shrink: 0;
}
.option-checkbox:hover .checkmark {
border-color: var(--color-accent);
background-color: var(--color-accent-light);
}
.option-checkbox input:checked ~ .checkmark {
background-color: var(--color-accent);
border-color: var(--color-accent);
}
.checkmark:after {
content: "";
position: absolute;
display: none;
left: 5px;
top: 2px;
width: 4px;
height: 8px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.option-checkbox input:checked ~ .checkmark:after {
display: block;
}
.option-label {
font-size: 0.75rem;
color: var(--color-text-secondary);
white-space: nowrap;
}
/* Slide down animation */
.slide-down-enter-active,
.slide-down-leave-active {
transition: all var(--transition-normal);
}
.slide-down-enter-from {
opacity: 0;
transform: translateY(-10px);
}
.slide-down-leave-to {
opacity: 0;
transform: translateY(-10px);
}
.message-input {
flex: 1;
min-width: 0;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
resize: none;
min-height: 44px;
max-height: 160px;
transition: all var(--transition-normal);
font-family: inherit;
line-height: 1.5;
letter-spacing: 0.01em;
box-shadow: var(--shadow-inner);
}
.message-input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px var(--color-accent-light), var(--shadow-inner);
background-color: var(--color-bg-secondary);
}
.message-input::placeholder {
color: var(--color-text-muted);
opacity: 0.7;
}
.send-btn {
width: 44px;
height: 44px;
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
background: var(--gradient-primary);
color: white;
border: none;
cursor: pointer;
transition: all var(--transition-normal);
flex-shrink: 0;
box-shadow: var(--shadow-md), 0 0 0 1px rgba(91, 127, 255, 0.1);
position: relative;
overflow: hidden;
}
.send-btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.send-btn:hover::before {
width: 300px;
height: 300px;
}
.send-btn:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-xl), 0 0 0 2px rgba(91, 127, 255, 0.2);
}
.send-btn:active {
transform: translateY(0);
}
.send-btn svg {
position: relative;
z-index: 1;
transition: transform var(--transition-normal);
}
.send-btn:hover svg {
transform: scale(1.1) rotate(-5deg);
}
</style>

View File

@@ -0,0 +1,3 @@
.chat-input {
/* Chat input styles */
}

View File

@@ -0,0 +1,17 @@
import { ref } from 'vue';
export function useChatInput() {
const inputText = ref('');
function sendMessage() {
if (inputText.value.trim()) {
console.log('Sending:', inputText.value);
inputText.value = '';
}
}
return {
inputText,
sendMessage,
};
}

View File

@@ -0,0 +1 @@
# MessageList Feature

View File

@@ -0,0 +1,481 @@
<template>
<div class="message-list">
<div class="messages-container">
<div class="message user-message">
<div class="message-avatar"></div>
<div class="message-content">
<p v-html="renderMessageContent(userMessage)"></p>
<div class="message-actions">
<button class="action-btn" title="Edit">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button class="action-btn" title="Delete">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
<button class="action-btn" title="Copy">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<span class="message-time">10:30 AM</span>
</div>
</div>
<div class="message assistant-message">
<div class="message-avatar"></div>
<div class="message-content">
<p v-html="renderMessageContent(assistantMessage)"></p>
<div class="message-actions">
<button class="action-btn" title="Edit">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button class="action-btn" title="Delete">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
<button class="action-btn" title="Copy">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<span class="message-time">10:31 AM</span>
</div>
</div>
<div class="message user-message">
<div class="message-avatar"></div>
<div class="message-content">
<p>Another message to show the conversation flow.</p>
<span class="message-time">10:32 AM</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, watch } from 'vue';
// Props from parent component - Vue automatically unwraps refs in templates
const props = defineProps<{
renderMarkdown?: boolean;
renderHTML?: boolean;
}>();
// Use props or default to true
const renderMarkdown = computed(() => {
const value = props.renderMarkdown ?? true;
console.log('[MessageList] renderMarkdown:', value);
return value;
});
const renderHTML = computed(() => {
const value = props.renderHTML ?? true;
console.log('[MessageList] renderHTML:', value);
return value;
});
// Watch for changes
watch(renderMarkdown, (newVal) => {
console.log('[MessageList] renderMarkdown changed to:', newVal);
});
watch(renderHTML, (newVal) => {
console.log('[MessageList] renderHTML changed to:', newVal);
});
// Simple Markdown parser for testing (no external dependencies)
function simpleMarkdownParse(text: string): string {
let html = text;
// If HTML rendering is disabled, escape HTML first
if (!renderHTML.value) {
html = escapeHtml(html);
}
// Code blocks (must be processed before other rules to avoid conflicts)
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
const escapedCode = renderHTML.value ? code.trim() : escapeHtml(code.trim());
return `<pre><code class="language-${lang || 'text'}">${escapedCode}</code></pre>`;
});
// Inline code (must be processed before other inline rules)
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
// Headers
html = html.replace(/^### (.+)$/gim, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gim, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gim, '<h1>$1</h1>');
// Bold and Italic (order matters: *** before ** before *)
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
// Horizontal rule
html = html.replace(/^---$/gim, '<hr>');
// Blockquotes
html = html.replace(/^&gt; (.+)$/gim, '<blockquote>$1</blockquote>');
html = html.replace(/^> (.+)$/gim, '<blockquote>$1</blockquote>');
// Unordered lists
const ulPattern = /^(?:[-*+]\s+.+\n?)+/gm;
html = html.replace(ulPattern, (match) => {
const items = match.trim().split('\n').map(line => {
const content = line.replace(/^[-*+]\s+/, '');
return `<li>${content}</li>`;
}).join('');
return `<ul>${items}</ul>`;
});
// Ordered lists
const olPattern = /^(?:\d+\.\s+.+\n?)+/gm;
html = html.replace(olPattern, (match) => {
const items = match.trim().split('\n').map(line => {
const content = line.replace(/^\d+\.\s+/, '');
return `<li>${content}</li>`;
}).join('');
return `<ol>${items}</ol>`;
});
// Line breaks (convert newlines to <br>, but not inside pre tags)
html = html.replace(/\n/g, '<br>');
return html;
}
// Escape HTML special characters
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function renderMessageContent(content: string): string {
console.log('[renderMessageContent] Input:', content.substring(0, 50));
console.log('[renderMessageContent] renderMarkdown:', renderMarkdown.value, 'renderHTML:', renderHTML.value);
// If neither markdown nor HTML rendering is enabled, escape and return plain text
if (!renderMarkdown.value && !renderHTML.value) {
const result = escapeHtml(content);
console.log('[renderMessageContent] Both disabled, escaped:', result.substring(0, 50));
return result;
}
// If markdown rendering is enabled, parse it (HTML rendering controls whether HTML tags are escaped)
if (renderMarkdown.value) {
const result = simpleMarkdownParse(content);
console.log('[renderMessageContent] Markdown parsed, result length:', result.length);
return result;
}
// If only HTML rendering is enabled (no markdown), return as-is
if (renderHTML.value) {
console.log('[renderMessageContent] HTML only, returning as-is');
return content;
}
// HTML rendering disabled, no markdown - escape everything
const result = escapeHtml(content);
console.log('[renderMessageContent] HTML disabled, escaped:', result.substring(0, 50));
return result;
}
// Sample messages for testing - using LLM provided examples
const userMessage = '你好!这是一个**测试消息**,包含以下功能:\n\n1. **粗体文本**\n2. *斜体文本*\n3. `行内代码`\n4. [链接示例](https://example.com)';
const assistantMessage = [
'## Markdown 渲染测试',
'',
'这是一个功能丰富的回复,展示各种 Markdown 语法:',
'',
'### 列表示例',
'- 无序列表项 1',
'- 无序列表项 2',
' - 嵌套列表项',
'- 无序列表项 3',
'',
'1. 有序列表项 1',
'2. 有序列表项 2',
'3. 有序列表项 3',
'',
'### 引用示例',
'> 这是一段引用文本',
'> 可以有多行内容',
'',
'### 代码示例',
'行内代码:`const message = "Hello";`',
'',
'代码块:',
'```javascript',
'function greet(name) {',
' return `Hello, ${name}!`;',
'}',
'```',
'',
'### HTML 标签测试',
'<div style="color: #5b7fff; font-weight: bold;">这是自定义样式的 HTML 文本</div>',
'',
'---',
'',
'**粗体**、*斜体*、***粗斜体*** 都可以正常显示!'
].join('\n');
</script>
<style scoped>
.message-list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: var(--spacing-lg);
display: flex;
flex-direction: column;
width: 100%;
}
.messages-container {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
width: 100%;
height: 100%;
}
.message {
display: flex;
gap: var(--spacing-md);
max-width: 75%;
animation: fadeIn 0.4s var(--transition-smooth);
}
.user-message {
align-self: flex-end;
flex-direction: row-reverse;
}
.assistant-message {
align-self: flex-start;
}
.message-avatar {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
flex-shrink: 0;
background: linear-gradient(135deg, #c5d0ff, #dce4ff);
box-shadow: var(--shadow-sm);
position: relative;
}
.message-avatar::after {
content: '';
position: absolute;
inset: 2px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4), transparent);
}
.user-message .message-avatar {
background: var(--gradient-primary);
box-shadow: var(--shadow-md), 0 0 0 2px var(--color-accent-light);
}
.message-content {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
min-width: 0;
}
.message-content p {
margin: 0;
padding: var(--spacing-md);
border-radius: var(--radius-lg);
line-height: 1.6;
font-size: 0.9rem;
letter-spacing: 0.01em;
word-wrap: break-word;
overflow-wrap: break-word;
}
/* Markdown rendered content styles */
.message-content p :deep(strong),
.message-content p :deep(b) {
font-weight: 600;
}
.message-content p :deep(em),
.message-content p :deep(i) {
font-style: italic;
}
.message-content p :deep(code) {
background-color: rgba(0, 0, 0, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.85em;
}
.user-message .message-content p :deep(code) {
background-color: rgba(255, 255, 255, 0.2);
}
.message-content p :deep(pre) {
background-color: rgba(0, 0, 0, 0.15);
padding: var(--spacing-md);
border-radius: var(--radius-md);
overflow-x: auto;
margin: var(--spacing-sm) 0;
}
.user-message .message-content p :deep(pre) {
background-color: rgba(255, 255, 255, 0.15);
}
.message-content p :deep(pre code) {
background: none;
padding: 0;
}
.message-content p :deep(ul),
.message-content p :deep(ol) {
margin: var(--spacing-sm) 0;
padding-left: var(--spacing-lg);
}
.message-content p :deep(li) {
margin: var(--spacing-xs) 0;
}
.message-content p :deep(blockquote) {
border-left: 3px solid var(--color-accent);
padding-left: var(--spacing-md);
margin: var(--spacing-sm) 0;
opacity: 0.8;
}
.message-content p :deep(h1),
.message-content p :deep(h2),
.message-content p :deep(h3),
.message-content p :deep(h4),
.message-content p :deep(h5),
.message-content p :deep(h6) {
margin: var(--spacing-md) 0 var(--spacing-sm) 0;
font-weight: 600;
line-height: 1.4;
}
.message-content p :deep(h1) { font-size: 1.5em; }
.message-content p :deep(h2) { font-size: 1.3em; }
.message-content p :deep(h3) { font-size: 1.1em; }
.message-content p :deep(a) {
color: inherit;
text-decoration: underline;
opacity: 0.8;
}
.message-content p :deep(a:hover) {
opacity: 1;
}
.message-actions {
display: flex;
gap: var(--spacing-xs);
opacity: 0;
transition: opacity var(--transition-fast);
margin-top: var(--spacing-xs);
}
.message-content:hover .message-actions {
opacity: 1;
}
.message-actions .action-btn {
width: 24px;
height: 24px;
padding: 0;
border: none;
background: transparent;
color: var(--color-text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-fast);
}
.message-actions .action-btn:hover {
background-color: var(--color-accent-light);
color: var(--color-accent);
transform: scale(1.1);
}
.user-message .message-content p {
background: var(--gradient-primary);
color: var(--color-text-inverse);
border-bottom-right-radius: var(--radius-sm);
box-shadow: var(--shadow-md), 0 0 0 1px rgba(91, 127, 255, 0.1);
}
.assistant-message .message-content p {
background-color: var(--color-bg-secondary);
color: var(--color-text-primary);
border: 1px solid var(--color-border-light);
border-bottom-left-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
}
.message-time {
font-size: 0.7rem;
color: var(--color-text-muted);
padding: 0 var(--spacing-xs);
opacity: 0.8;
}
.user-message .message-time {
text-align: right;
}
/* Custom scrollbar for webkit browsers */
.message-list::-webkit-scrollbar {
width: 6px;
}
.message-list::-webkit-scrollbar-track {
background: transparent;
}
.message-list::-webkit-scrollbar-thumb {
background-color: var(--color-border);
border-radius: var(--radius-full);
border: 2px solid transparent;
background-clip: content-box;
}
.message-list::-webkit-scrollbar-thumb:hover {
background-color: var(--color-text-muted);
}
</style>

View File

@@ -0,0 +1,3 @@
.message-list {
/* Message list styles */
}

View File

@@ -0,0 +1,14 @@
import { ref } from 'vue';
export function useMessageList() {
const messages = ref<Array<{ id: string; role: string; content: string }>>([]);
function addMessage(message: { id: string; role: string; content: string }) {
messages.value.push(message);
}
return {
messages,
addMessage,
};
}

View File

@@ -0,0 +1 @@
# CenterPanel Stores

View File

@@ -0,0 +1,17 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useCenterPanelStore = defineStore('centerPanel', () => {
// State
const isTyping = ref(false);
// Actions
function setTyping(typing: boolean) {
isTyping.value = typing;
}
return {
isTyping,
setTyping,
};
});

View File

@@ -0,0 +1,99 @@
<template>
<div class="left-panel">
<TabSwitcher v-model="activeTab" :tabs="tabs">
<!-- Placeholder components for each tab -->
<div v-if="activeTab === 'world'" class="tab-placeholder">
<h3>World Info</h3>
<p>世界书管理页面待实现</p>
</div>
<div v-if="activeTab === 'api'" class="tab-placeholder">
<h3>API Configuration</h3>
<p>API 配置页面待实现</p>
</div>
<div v-if="activeTab === 'presets'" class="tab-placeholder">
<h3>Presets</h3>
<p>预设管理页面待实现</p>
</div>
<div v-if="activeTab === 'gallery'" class="tab-placeholder">
<h3>Gallery</h3>
<p>画廊页面待实现</p>
</div>
<div v-if="activeTab === 'characters'" class="tab-placeholder">
<h3>AI Character Cards</h3>
<p>AI 角色卡管理页面待实现</p>
</div>
</TabSwitcher>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import TabSwitcher from '@/components/common/TabSwitcher/TabSwitcher.vue';
const activeTab = ref('world');
const tabs = [
{ id: 'world', label: 'World' },
{ id: 'api', label: 'API' },
{ id: 'presets', label: 'Presets' },
{ id: 'gallery', label: 'Gallery' },
{ id: 'characters', label: 'Characters' }
];
</script>
<style scoped>
.left-panel {
flex: 0 0 20%;
min-width: 0;
max-width: none;
background-color: var(--color-bg-secondary);
border-right: 1px solid var(--color-border);
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-xs);
transition: box-shadow var(--transition-normal);
}
.tab-placeholder {
padding: var(--spacing-lg);
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: var(--color-text-muted);
}
.tab-placeholder h3 {
font-size: 1rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: var(--spacing-sm);
}
.tab-placeholder p {
font-size: 0.85rem;
line-height: 1.5;
}
/* Custom scrollbar for webkit browsers */
.left-panel::-webkit-scrollbar {
width: 6px;
}
.left-panel::-webkit-scrollbar-track {
background: transparent;
}
.left-panel::-webkit-scrollbar-thumb {
background-color: var(--color-border);
border-radius: var(--radius-full);
}
.left-panel::-webkit-scrollbar-thumb:hover {
background-color: var(--color-text-muted);
}
</style>

View File

@@ -0,0 +1 @@
# LeftPanel Features

View File

@@ -0,0 +1 @@
# CharacterList Feature

View File

@@ -0,0 +1,177 @@
<template>
<div class="character-list">
<div class="section-header">
<h3>Characters</h3>
<button class="add-btn" title="Add Character">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
<div class="character-items">
<div class="character-item">
<div class="character-avatar"></div>
<div class="character-info">
<h4>Character Name</h4>
<p>Brief description...</p>
</div>
</div>
<div class="character-item">
<div class="character-avatar"></div>
<div class="character-info">
<h4>Another Character</h4>
<p>Another brief description...</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// CharacterList component
</script>
<style scoped>
.character-list {
flex: 1;
min-height: 0;
padding: var(--spacing-md);
overflow-y: auto;
width: 100%;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--color-border-light);
}
.section-header h3 {
font-size: 1rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
letter-spacing: -0.01em;
}
.add-btn {
width: 30px;
height: 30px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
transition: all var(--transition-normal);
cursor: pointer;
flex-shrink: 0;
}
.add-btn:hover {
background-color: var(--color-accent-light);
color: var(--color-accent);
border-color: var(--color-accent);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.add-btn:active {
transform: translateY(0);
}
.character-items {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
width: 100%;
}
.character-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
border-radius: var(--radius-md);
background-color: var(--color-bg-tertiary);
border: 1px solid var(--color-border-light);
transition: all var(--transition-normal);
cursor: pointer;
position: relative;
overflow: hidden;
width: 100%;
}
.character-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--gradient-primary);
transform: scaleY(0);
transition: transform var(--transition-normal);
}
.character-item:hover {
background-color: var(--color-bg-elevated);
border-color: var(--color-border-focus);
box-shadow: var(--shadow-md);
transform: translateX(4px);
}
.character-item:hover::before {
transform: scaleY(1);
}
.character-avatar {
width: 38px;
height: 38px;
border-radius: var(--radius-md);
background: linear-gradient(135deg, #c5d0ff, #dce4ff);
flex-shrink: 0;
box-shadow: var(--shadow-sm);
position: relative;
}
.character-avatar::after {
content: '';
position: absolute;
inset: 2px;
border-radius: calc(var(--radius-md) - 2px);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), transparent);
}
.character-info {
flex: 1;
min-width: 0;
}
.character-info h4 {
font-size: 0.9rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 2px 0;
letter-spacing: -0.01em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.character-info p {
font-size: 0.8rem;
color: var(--color-text-secondary);
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,3 @@
.character-list {
/* Character list styles */
}

View File

@@ -0,0 +1,14 @@
import { ref } from 'vue';
export function useCharacterList() {
const searchQuery = ref('');
function filterCharacters(query: string) {
searchQuery.value = query;
}
return {
searchQuery,
filterCharacters,
};
}

View File

@@ -0,0 +1 @@
# ChatHistory Feature

View File

@@ -0,0 +1,165 @@
<template>
<div class="chat-history">
<div class="section-header">
<h3>Chat History</h3>
</div>
<div class="history-items">
<div class="history-item active">
<div class="history-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
</svg>
</div>
<div class="history-info">
<h4>Current Conversation</h4>
<p>Last message preview...</p>
</div>
</div>
<div class="history-item">
<div class="history-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
</svg>
</div>
<div class="history-info">
<h4>Previous Chat</h4>
<p>Another conversation...</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// ChatHistory component
</script>
<style scoped>
.chat-history {
flex: 1;
min-height: 0;
padding: var(--spacing-md);
overflow-y: auto;
width: 100%;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
}
.section-header h3 {
font-size: 1rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
letter-spacing: -0.01em;
}
.history-items {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
width: 100%;
}
.history-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
border-radius: var(--radius-md);
background-color: var(--color-bg-tertiary);
border: 1px solid var(--color-border-light);
transition: all var(--transition-normal);
cursor: pointer;
position: relative;
overflow: hidden;
width: 100%;
}
.history-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--gradient-primary);
transform: scaleY(0);
transition: transform var(--transition-normal);
}
.history-item:hover {
background-color: var(--color-bg-elevated);
border-color: var(--color-border-focus);
box-shadow: var(--shadow-md);
transform: translateX(4px);
}
.history-item:hover::before {
transform: scaleY(1);
}
.history-item.active {
background-color: var(--color-accent-light);
border-color: var(--color-accent);
}
.history-item.active::before {
transform: scaleY(1);
}
.history-icon {
width: 32px;
height: 32px;
border-radius: var(--radius-md);
background-color: var(--color-bg-elevated);
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
flex-shrink: 0;
box-shadow: var(--shadow-sm);
}
.history-item.active .history-icon {
background: var(--gradient-primary);
color: white;
box-shadow: var(--shadow-md), 0 0 0 2px var(--color-accent-light);
}
.history-info {
flex: 1;
min-width: 0;
}
.history-info h4 {
font-size: 0.85rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 2px 0;
letter-spacing: -0.01em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-item.active .history-info h4 {
color: var(--color-accent);
}
.history-info p {
font-size: 0.75rem;
color: var(--color-text-secondary);
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,3 @@
.chat-history {
/* Chat history styles */
}

View File

@@ -0,0 +1,15 @@
import { ref } from 'vue';
export function useChatHistory() {
const histories = ref<Array<{ id: string; title: string }>>([]);
function loadHistory() {
// TODO: Load chat history
console.log('Loading chat history...');
}
return {
histories,
loadHistory,
};
}

View File

@@ -0,0 +1,3 @@
.left-panel {
/* Left panel base styles */
}

View File

@@ -0,0 +1 @@
# LeftPanel Stores

View File

@@ -0,0 +1,17 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useLeftPanelStore = defineStore('leftPanel', () => {
// State
const activeTab = ref<'characters' | 'history'>('characters');
// Actions
function setActiveTab(tab: 'characters' | 'history') {
activeTab.value = tab;
}
return {
activeTab,
setActiveTab,
};
});

View File

@@ -10,10 +10,10 @@
</template>
<script setup lang="ts">
import TopBar from '@/components/TopBar/TopBar.vue';
import LeftPanel from '@/components/LeftPanel/LeftPanel.vue';
import CenterPanel from '@/components/CenterPanel/CenterPanel.vue';
import RightPanel from '@/components/RightPanel/RightPanel.vue';
import TopBar from '../TopBar/TopBar.vue';
import LeftPanel from '../LeftPanel/LeftPanel.vue';
import CenterPanel from '../CenterPanel/CenterPanel.vue';
import RightPanel from '../RightPanel/RightPanel.vue';
</script>
<style scoped>
@@ -24,11 +24,25 @@ import RightPanel from '@/components/RightPanel/RightPanel.vue';
height: 100vh;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
overflow: hidden;
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
position: relative;
/* Ensure panels maintain their proportions */
min-width: 0;
}
/* Smooth transitions for theme changes */
.main-layout,
.main-layout * {
transition: background-color var(--transition-normal),
color var(--transition-normal),
border-color var(--transition-normal),
box-shadow var(--transition-normal);
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<div class="right-panel">
<DualTabSwitcher v-model="selectedTabs" :tabs="tabs">
<div class="panel-content">
<div
v-if="selectedTabs.includes('rag')"
class="panel-section"
:class="{ 'has-divider': selectedTabs.length === 2 }"
>
<div class="tab-placeholder">
<h3>RAG Information</h3>
<p>RAG 信息页面待实现</p>
</div>
</div>
<div
v-if="selectedTabs.includes('worldentries')"
class="panel-section"
:class="{ 'has-divider': selectedTabs.length === 2 && selectedTabs.indexOf('worldentries') < selectedTabs.length - 1 }"
>
<div class="tab-placeholder">
<h3>World Info Entries</h3>
<p>世界书条目页面待实现</p>
</div>
</div>
<div
v-if="selectedTabs.includes('dynamictable')"
class="panel-section"
:class="{ 'has-divider': selectedTabs.length === 2 && selectedTabs.indexOf('dynamictable') < selectedTabs.length - 1 }"
>
<div class="tab-placeholder">
<h3>Dynamic Table</h3>
<p>动态表格页面待实现</p>
</div>
</div>
<div
v-if="selectedTabs.includes('dice')"
class="panel-section"
>
<div class="tab-placeholder">
<h3>Dice Roller</h3>
<p>骰子页面待实现</p>
</div>
</div>
</div>
</DualTabSwitcher>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import DualTabSwitcher from '@/components/common/DualTabSwitcher/DualTabSwitcher.vue';
// 默认选中两个页面
const selectedTabs = ref(['rag', 'worldentries']);
const tabs = [
{ id: 'rag', label: 'RAG' },
{ id: 'worldentries', label: 'World Entries' },
{ id: 'dynamictable', label: 'Dynamic Table' },
{ id: 'dice', label: 'Dice' }
];
</script>
<style scoped>
.right-panel {
flex: 0 0 20%;
min-width: 0;
max-width: none;
background-color: var(--color-bg-secondary);
border-left: 1px solid var(--color-border);
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-xs);
transition: box-shadow var(--transition-normal);
}
.panel-content {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.panel-section {
flex: 1;
min-height: 0;
overflow-y: auto;
}
/* 当有两个页面时,第一个页面添加底部分隔线 */
.panel-section.has-divider {
border-bottom: 1px solid var(--color-border-light);
}
/* 当只有一个页面选中时,占据全部空间 */
.panel-section:only-child {
flex: 1;
}
.tab-placeholder {
padding: var(--spacing-lg);
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: var(--color-text-muted);
}
.tab-placeholder h3 {
font-size: 1rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: var(--spacing-sm);
}
.tab-placeholder p {
font-size: 0.85rem;
line-height: 1.5;
}
/* Custom scrollbar for webkit browsers */
.right-panel::-webkit-scrollbar {
width: 6px;
}
.right-panel::-webkit-scrollbar-track {
background: transparent;
}
.right-panel::-webkit-scrollbar-thumb {
background-color: var(--color-border);
border-radius: var(--radius-full);
}
.right-panel::-webkit-scrollbar-thumb:hover {
background-color: var(--color-text-muted);
}
</style>

View File

@@ -0,0 +1 @@
# RightPanel Features

View File

@@ -0,0 +1 @@
# CharacterDetail Feature

View File

@@ -0,0 +1,267 @@
<template>
<div class="character-detail">
<div class="detail-header">
<h3>Character Details</h3>
<button class="edit-btn" @click="toggleEdit" title="Edit Character">
<svg v-if="!isEditing" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 11 12 14 22 4"></polyline>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</svg>
</button>
</div>
<div class="detail-content">
<div class="avatar-section">
<div class="character-avatar"></div>
<div class="avatar-actions">
<button class="action-btn" title="Change Avatar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</button>
</div>
</div>
<div class="info-section">
<div class="form-group">
<label for="char-name">Name</label>
<input
id="char-name"
type="text"
value="Character Name"
:disabled="!isEditing"
class="form-input"
/>
</div>
<div class="form-group">
<label for="char-desc">Description</label>
<textarea
id="char-desc"
value="A brief description of the character..."
:disabled="!isEditing"
class="form-textarea"
rows="3"
></textarea>
</div>
<div class="form-group">
<label for="char-personality">Personality</label>
<textarea
id="char-personality"
value="Friendly, curious, and helpful..."
:disabled="!isEditing"
class="form-textarea"
rows="3"
></textarea>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useCharacterDetail } from './useCharacterDetail';
const { isEditing, toggleEdit } = useCharacterDetail();
</script>
<style scoped>
.character-detail {
flex: 1;
min-height: 0;
padding: var(--spacing-lg);
overflow-y: auto;
width: 100%;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--color-border-light);
}
.detail-header h3 {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
letter-spacing: -0.02em;
}
.edit-btn {
width: 34px;
height: 34px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
transition: all var(--transition-normal);
cursor: pointer;
flex-shrink: 0;
}
.edit-btn:hover {
background-color: var(--color-accent-light);
color: var(--color-accent);
border-color: var(--color-accent);
transform: translateY(-2px) rotate(5deg);
box-shadow: var(--shadow-md);
}
.edit-btn:active {
transform: translateY(0) rotate(0);
}
.detail-content {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
width: 100%;
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-md);
width: 100%;
}
.character-avatar {
width: 100px;
height: 100px;
border-radius: var(--radius-xl);
background: linear-gradient(135deg, #c5d0ff, #dce4ff);
box-shadow: var(--shadow-lg), 0 0 0 1px rgba(91, 127, 255, 0.1);
position: relative;
overflow: hidden;
flex-shrink: 0;
}
.character-avatar::after {
content: '';
position: absolute;
inset: 3px;
border-radius: calc(var(--radius-xl) - 3px);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4), transparent);
}
.avatar-actions {
display: flex;
gap: var(--spacing-sm);
}
.action-btn {
width: 32px;
height: 32px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-bg-tertiary);
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
transition: all var(--transition-normal);
cursor: pointer;
flex-shrink: 0;
}
.action-btn:hover {
background-color: var(--color-accent-light);
color: var(--color-accent);
border-color: var(--color-accent);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.info-section {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
width: 100%;
}
.form-group {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
width: 100%;
}
.form-group label {
font-size: 0.85rem;
font-weight: 500;
color: var(--color-text-primary);
letter-spacing: -0.01em;
}
.form-input,
.form-textarea {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 0.9rem;
transition: all var(--transition-normal);
font-family: inherit;
line-height: 1.5;
letter-spacing: 0.01em;
}
.form-input:focus,
.form-textarea:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px var(--color-accent-light);
background-color: var(--color-bg-secondary);
}
.form-input:disabled,
.form-textarea:disabled {
background-color: var(--color-bg-subtle);
color: var(--color-text-secondary);
cursor: not-allowed;
opacity: 0.7;
}
.form-textarea {
resize: vertical;
min-height: 80px;
line-height: 1.5;
}
/* Custom scrollbar for webkit browsers */
.character-detail::-webkit-scrollbar {
width: 6px;
}
.character-detail::-webkit-scrollbar-track {
background: transparent;
}
.character-detail::-webkit-scrollbar-thumb {
background-color: var(--color-border);
border-radius: var(--radius-full);
border: 2px solid transparent;
background-clip: content-box;
}
.character-detail::-webkit-scrollbar-thumb:hover {
background-color: var(--color-text-muted);
}
</style>

View File

@@ -0,0 +1,3 @@
.character-detail {
/* Character detail styles */
}

View File

@@ -0,0 +1,14 @@
import { ref } from 'vue';
export function useCharacterDetail() {
const isEditing = ref(false);
function toggleEdit() {
isEditing.value = !isEditing.value;
}
return {
isEditing,
toggleEdit,
};
}

View File

@@ -0,0 +1 @@
# WorkflowEditor Feature

View File

@@ -0,0 +1,180 @@
<template>
<div class="workflow-editor">
<div class="section-header">
<h3>Workflow Editor</h3>
<button class="add-btn" title="Add Workflow">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
<div class="workflow-items">
<div class="workflow-item">
<div class="workflow-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 18 22 12 16 6"></polyline>
<polyline points="8 6 2 12 8 18"></polyline>
</svg>
</div>
<div class="workflow-info">
<h4>Sample Workflow</h4>
<p>A basic AI workflow...</p>
</div>
</div>
<div class="workflow-item">
<div class="workflow-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 18 22 12 16 6"></polyline>
<polyline points="8 6 2 12 8 18"></polyline>
</svg>
</div>
<div class="workflow-info">
<h4>Another Workflow</h4>
<p>More complex workflow...</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// WorkflowEditor component
</script>
<style scoped>
.workflow-editor {
flex: 1;
min-height: 0;
padding: var(--spacing-lg);
overflow-y: auto;
width: 100%;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
}
.section-header h3 {
font-size: 1rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
letter-spacing: -0.01em;
}
.add-btn {
width: 30px;
height: 30px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
transition: all var(--transition-normal);
cursor: pointer;
flex-shrink: 0;
}
.add-btn:hover {
background-color: var(--color-accent-light);
color: var(--color-accent);
border-color: var(--color-accent);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.add-btn:active {
transform: translateY(0);
}
.workflow-items {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
width: 100%;
}
.workflow-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
border-radius: var(--radius-md);
background-color: var(--color-bg-tertiary);
border: 1px solid var(--color-border-light);
transition: all var(--transition-normal);
cursor: pointer;
position: relative;
overflow: hidden;
width: 100%;
}
.workflow-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--gradient-primary);
transform: scaleY(0);
transition: transform var(--transition-normal);
}
.workflow-item:hover {
background-color: var(--color-bg-elevated);
border-color: var(--color-border-focus);
box-shadow: var(--shadow-md);
transform: translateX(4px);
}
.workflow-item:hover::before {
transform: scaleY(1);
}
.workflow-icon {
width: 38px;
height: 38px;
border-radius: var(--radius-md);
background-color: var(--color-bg-elevated);
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
flex-shrink: 0;
box-shadow: var(--shadow-sm);
}
.workflow-info {
flex: 1;
min-width: 0;
}
.workflow-info h4 {
font-size: 0.9rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 2px 0;
letter-spacing: -0.01em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.workflow-info p {
font-size: 0.8rem;
color: var(--color-text-secondary);
margin: 0;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,17 @@
import { ref } from 'vue';
export function useWorkflowEditor() {
const workflows = ref<Array<{ id: string; name: string }>>([]);
function addWorkflow(name: string) {
workflows.value.push({
id: Date.now().toString(),
name,
});
}
return {
workflows,
addWorkflow,
};
}

View File

@@ -0,0 +1,3 @@
.workflow-editor {
/* Workflow editor styles */
}

View File

@@ -0,0 +1,3 @@
.right-panel {
/* Right panel base styles */
}

View File

@@ -0,0 +1 @@
# RightPanel Stores

View File

@@ -0,0 +1,17 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useRightPanelStore = defineStore('rightPanel', () => {
// State
const activeTab = ref<'detail' | 'workflow'>('detail');
// Actions
function setActiveTab(tab: 'detail' | 'workflow') {
activeTab.value = tab;
}
return {
activeTab,
setActiveTab,
};
});

View File

@@ -0,0 +1,294 @@
<template>
<div class="top-bar">
<div class="top-bar-content">
<!-- Status Indicators -->
<div class="status-section">
<!-- Current Character -->
<div class="status-badge" title="当前玩家角色">
<span class="status-icon">😊</span>
<span class="status-label">角色名</span>
</div>
<!-- Current Model -->
<div class="status-badge" title="当前模型名(中转站)">
<span class="status-icon">🔌</span>
<span class="status-label">GPT-4</span>
</div>
<!-- Current Preset -->
<div class="status-badge" title="当前预设名">
<span class="status-icon"></span>
<span class="status-label">默认预设</span>
</div>
<!-- Active World Info (rightmost, can be longest) -->
<div class="status-badge world-info-badge" title="已激活全局世界书">
<span class="status-icon">📚</span>
<span class="status-label">世界书: 冒险魔法科技...</span>
</div>
</div>
<div class="actions-section">
<!-- Settings Button -->
<button class="action-btn" title="设置">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
</button>
<!-- Extensions Button -->
<button class="action-btn" title="扩展">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5z"></path>
<path d="M2 17l10 5 10-5"></path>
<path d="M2 12l10 5 10-5"></path>
</svg>
</button>
<!-- Theme Toggle -->
<button class="action-btn theme-toggle" @click="toggleTheme" :title="theme === 'light' ? '切换到夜间模式' : '切换到白天模式'">
<svg v-if="theme === 'light'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useTheme } from '@/composables/useTheme';
const { theme, toggleTheme } = useTheme();
</script>
<style scoped>
.top-bar {
height: 56px;
display: flex;
align-items: center;
background-color: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border-light);
box-shadow: var(--shadow-sm);
z-index: var(--z-sticky);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.top-bar-content {
width: 100%;
padding: 0 var(--spacing-lg);
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-md);
}
/* Status Section */
.status-section {
display: flex;
align-items: center;
gap: var(--spacing-md);
flex: 1;
justify-content: flex-start;
min-width: 0;
}
.logo-icon {
width: 32px;
height: 32px;
border-radius: var(--radius-md);
background: var(--gradient-primary);
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow-md), 0 0 0 1px rgba(91, 127, 255, 0.1);
position: relative;
overflow: hidden;
}
.logo-icon::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%);
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% {
transform: translate(-30%, -30%) rotate(0deg);
}
50% {
transform: translate(30%, 30%) rotate(180deg);
}
}
.logo-icon::after {
content: '';
width: 16px;
height: 16px;
background-color: white;
border-radius: 50%;
opacity: 0.95;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.app-title {
font-size: 1.15rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
letter-spacing: -0.02em;
background: linear-gradient(135deg, var(--color-text-primary) 0%, var(--color-text-secondary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.actions-section {
display: flex;
align-items: center;
gap: var(--spacing-sm);
flex-shrink: 0;
}
/* Status Badge Styles */
.status-badge {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border-light);
border-radius: var(--radius-lg);
transition: all var(--transition-fast);
cursor: default;
white-space: nowrap;
min-height: 36px;
}
.status-badge:hover {
border-color: var(--color-accent);
box-shadow: 0 0 0 3px var(--color-accent-light);
transform: translateY(-2px);
}
.status-icon {
font-size: 1.2rem;
line-height: 1;
flex-shrink: 0;
}
.status-label {
font-size: 0.95rem;
color: var(--color-text-primary);
font-weight: 500;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
/* World Info badge - allow more space */
.world-info-badge {
max-width: 350px;
}
.world-info-badge .status-label {
max-width: 280px;
}
.action-btn {
width: 42px;
height: 42px;
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
color: var(--color-text-secondary);
border: 1px solid transparent;
transition: all var(--transition-normal);
cursor: pointer;
position: relative;
}
.action-btn::before {
content: '';
position: absolute;
inset: 0;
border-radius: var(--radius-lg);
background: var(--color-accent-light);
opacity: 0;
transition: opacity var(--transition-fast);
}
.action-btn:hover {
color: var(--color-accent);
border-color: var(--color-accent);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.action-btn:hover::before {
opacity: 1;
}
.action-btn:active {
transform: translateY(0);
}
.action-btn svg {
position: relative;
z-index: 1;
transition: transform var(--transition-normal);
}
.action-btn:hover svg {
transform: scale(1.1) rotate(5deg);
}
/* Theme toggle specific styles */
.theme-toggle {
position: relative;
}
.theme-toggle::after {
content: '';
position: absolute;
inset: 0;
border-radius: var(--radius-lg);
background: var(--gradient-primary);
opacity: 0;
transition: opacity var(--transition-normal);
}
.theme-toggle:hover::after {
opacity: 0.1;
}
.theme-toggle svg {
position: relative;
z-index: 1;
}
.theme-toggle:hover svg {
transform: scale(1.15) rotate(15deg);
color: var(--color-accent);
}
</style>

View File

@@ -0,0 +1 @@
# TopBar Features

View File

@@ -0,0 +1 @@
# TopBar Stores

View File

@@ -0,0 +1,11 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useTopBarStore = defineStore('topBar', () => {
// State
const title = ref('SillyTavern Repalice');
return {
title,
};
});

View File

@@ -0,0 +1,3 @@
.top-bar {
/* Top bar base styles */
}

View File

@@ -15,17 +15,17 @@ const router = createRouter({
{
path: 'chat',
name: 'Chat',
component: () => import('@/components/CenterPanel/CenterPanel.vue'),
component: () => import('@/layouts/CenterPanel/CenterPanel.vue'),
},
{
path: 'characters',
name: 'Characters',
component: () => import('@/components/LeftPanel/LeftPanel.vue'),
component: () => import('@/layouts/LeftPanel/LeftPanel.vue'),
},
{
path: 'character/:id',
name: 'CharacterDetail',
component: () => import('@/components/RightPanel/RightPanel.vue'),
component: () => import('@/layouts/RightPanel/features/CharacterDetail/CharacterDetail.vue'),
},
],
},

View File

@@ -0,0 +1 @@
export { useAppStore } from './useAppStore';

View File

@@ -14,6 +14,8 @@ body,
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
ul,
@@ -47,3 +49,91 @@ video {
height: auto;
display: block;
}
/* Global animations and transitions */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
@keyframes shimmer {
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
}
/* Apply subtle animations to interactive elements */
button,
.action-btn,
.add-btn,
.edit-btn,
.send-btn {
transition: all var(--transition-normal);
}
button:hover,
.action-btn:hover,
.add-btn:hover,
.edit-btn:hover,
.send-btn:hover {
animation: pulse 0.4s ease;
}
/* Smooth scrolling for the entire page */
html {
scroll-behavior: smooth;
}
/* Selection styling */
::selection {
background-color: var(--color-accent-light);
color: var(--color-accent);
}
::-moz-selection {
background-color: var(--color-accent-light);
color: var(--color-accent);
}

View File

@@ -1,47 +1,68 @@
/* CSS Variables */
:root {
/* Colors - Dark Theme (Default) */
--color-bg-primary: #1a1a1a;
--color-bg-secondary: #252525;
--color-bg-tertiary: #303030;
/* Colors - Dark Theme (Default) - Elegant night mode */
--color-bg-primary: #0f1115;
--color-bg-secondary: #161920;
--color-bg-tertiary: #1c1f27;
--color-bg-elevated: #1e2129;
--color-bg-subtle: #14171d;
--color-text-primary: #e0e0e0;
--color-text-secondary: #b0b0b0;
--color-text-muted: #808080;
--color-text-primary: #e8eaed;
--color-text-secondary: #9aa0a6;
--color-text-muted: #6b7280;
--color-text-inverse: #0f1115;
--color-border: #404040;
--color-border-light: #505050;
--color-border: #2d3139;
--color-border-light: #242830;
--color-border-focus: #3d424d;
--color-accent: #6c63ff;
--color-accent-hover: #7b73ff;
--color-accent-active: #5a52d5;
--color-accent: #6d8cff;
--color-accent-hover: #7d9bff;
--color-accent-active: #5b7fff;
--color-accent-light: rgba(109, 140, 255, 0.1);
--color-accent-ultra-light: rgba(109, 140, 255, 0.05);
--color-success: #4caf50;
--color-warning: #ff9800;
--color-error: #f44336;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-info: #3b82f6;
/* Spacing */
/* Gradient backgrounds */
--gradient-primary: linear-gradient(135deg, #6d8cff 0%, #8da7ff 100%);
--gradient-subtle: linear-gradient(180deg, rgba(109, 140, 255, 0.05) 0%, transparent 100%);
/* Spacing - Compact but comfortable */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-sm: 6px;
--spacing-md: 12px;
--spacing-lg: 18px;
--spacing-xl: 24px;
--spacing-2xl: 32px;
--spacing-3xl: 40px;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
/* Border Radius - Softer, more refined */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 20px;
--radius-2xl: 24px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
/* Shadows - Subtle and layered for depth */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.15);
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.2), 0 1px 2px rgba(0, 0, 0, 0.15);
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.2);
--shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.3), 0 4px 8px rgba(0, 0, 0, 0.25);
--shadow-xl: 0 12px 24px rgba(0, 0, 0, 0.35), 0 6px 12px rgba(0, 0, 0, 0.3);
--shadow-2xl: 0 20px 40px rgba(0, 0, 0, 0.4), 0 10px 20px rgba(0, 0, 0, 0.35);
--shadow-inner: inset 0 2px 4px rgba(0, 0, 0, 0.15);
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
--transition-slow: 350ms ease;
/* Transitions - Smooth and elegant */
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-bounce: 500ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
--transition-smooth: 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
/* Z-index layers */
--z-dropdown: 1000;
@@ -53,20 +74,45 @@
--z-tooltip: 1070;
}
/* Light Theme */
[data-theme='light'] {
--color-bg-primary: #ffffff;
--color-bg-secondary: #f5f5f5;
--color-bg-tertiary: #e8e8e8;
--color-text-primary: #212121;
--color-text-secondary: #616161;
--color-text-muted: #9e9e9e;
--color-border: #e0e0e0;
--color-border-light: #d0d0d0;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.15);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.2);
/* Global theme transition */
*, *::before, *::after {
transition: background-color var(--transition-normal),
border-color var(--transition-normal),
color var(--transition-normal),
box-shadow var(--transition-normal);
}
/* Light Theme - Bright and clean day mode */
[data-theme='light'] {
--color-bg-primary: #fafbfc;
--color-bg-secondary: #ffffff;
--color-bg-tertiary: #f5f6f8;
--color-bg-elevated: #ffffff;
--color-bg-subtle: #f0f2f5;
--color-text-primary: #1a1d21;
--color-text-secondary: #5a6169;
--color-text-muted: #8b9199;
--color-text-inverse: #ffffff;
--color-border: #e8eaed;
--color-border-light: #f0f1f3;
--color-border-focus: #d1d5db;
--color-accent: #5b7fff;
--color-accent-hover: #4a6ef5;
--color-accent-active: #3d5ce0;
--color-accent-light: rgba(91, 127, 255, 0.06);
--color-accent-ultra-light: rgba(91, 127, 255, 0.03);
--gradient-primary: linear-gradient(135deg, #5b7fff 0%, #7c9cff 100%);
--gradient-subtle: linear-gradient(180deg, rgba(91, 127, 255, 0.03) 0%, transparent 100%);
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.03);
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.03);
--shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.06), 0 4px 8px rgba(0, 0, 0, 0.04);
--shadow-xl: 0 12px 24px rgba(0, 0, 0, 0.07), 0 6px 12px rgba(0, 0, 0, 0.05);
--shadow-2xl: 0 20px 40px rgba(0, 0, 0, 0.08), 0 10px 20px rgba(0, 0, 0, 0.06);
--shadow-inner: inset 0 2px 4px rgba(0, 0, 0, 0.03);
}

View File

@@ -22,6 +22,6 @@
"@shared/*": ["../shared/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/env.d.ts"],
"references": [{ "path": "./tsconfig.node.json" }]
}