前端修饰与渲染处理
This commit is contained in:
51
client/src/layouts/CenterPanel/CenterPanel.vue
Normal file
51
client/src/layouts/CenterPanel/CenterPanel.vue
Normal 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>
|
||||
3
client/src/layouts/CenterPanel/center-panel.css
Normal file
3
client/src/layouts/CenterPanel/center-panel.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.center-panel {
|
||||
/* Center panel base styles */
|
||||
}
|
||||
1
client/src/layouts/CenterPanel/features/.gitkeep
Normal file
1
client/src/layouts/CenterPanel/features/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# CenterPanel Features
|
||||
@@ -0,0 +1 @@
|
||||
# ChatInput Feature
|
||||
322
client/src/layouts/CenterPanel/features/ChatInput/ChatInput.vue
Normal file
322
client/src/layouts/CenterPanel/features/ChatInput/ChatInput.vue
Normal 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>
|
||||
@@ -0,0 +1,3 @@
|
||||
.chat-input {
|
||||
/* Chat input styles */
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# MessageList Feature
|
||||
@@ -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(/^> (.+)$/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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,3 @@
|
||||
.message-list {
|
||||
/* Message list styles */
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
1
client/src/layouts/CenterPanel/stores/.gitkeep
Normal file
1
client/src/layouts/CenterPanel/stores/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# CenterPanel Stores
|
||||
17
client/src/layouts/CenterPanel/stores/useCenterPanelStore.ts
Normal file
17
client/src/layouts/CenterPanel/stores/useCenterPanelStore.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user