优化会话列表,优化头像显示
This commit is contained in:
@@ -69,9 +69,12 @@ router.post('/chat/stream', auth(false), async (req: AuthRequest, res: Response)
|
||||
|
||||
router.post('/conversations', auth(), async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { modelId, messages, title } = req.body;
|
||||
const conv = await Conversation.create({ userId: req.userId, modelId, messages, title });
|
||||
res.json(conv);
|
||||
const { modelId, messages, title } = req.body;
|
||||
// clean messages: remove system/transient or empty entries
|
||||
const cleaned = (messages||[]).filter((m:any)=>m && typeof m.content==='string' && m.content.trim().length>0);
|
||||
const computedTitle = title || (cleaned?.[0]?.content ? String(cleaned[0].content).slice(0, 60) : '新对话');
|
||||
const conv = await Conversation.create({ userId: req.userId, modelId, messages: cleaned, title: computedTitle });
|
||||
res.json(conv);
|
||||
} catch (e:any) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@
|
||||
<div class="sidebar-bottom">
|
||||
<div class="avatar-area left-align" style="position:relative;">
|
||||
<button class="avatar-btn" @click="authStore.isAuthed ? (showUserMenu = !showUserMenu) : goLogin()">
|
||||
<img v-if="authStore.user" :src="avatarUrl(userEmail)" alt="avatar" />
|
||||
<img v-if="authStore.user" :src="avatarUrlForName(authStore.user.userName || authStore.user.email)" alt="avatar" />
|
||||
<div v-else class="avatar-fallback">{{ userInitial }}</div>
|
||||
</button>
|
||||
<div v-if="showUserMenu" class="user-menu">
|
||||
@@ -69,8 +69,8 @@ function logout() { authStore.logout(); showUserMenu.value = false; router.push(
|
||||
function toggleTheme() { themeStore.toggle(); }
|
||||
async function handleNewConv() {
|
||||
if (authStore.isAuthed) {
|
||||
// create empty conversation on server and navigate
|
||||
const conv = await convStore.createConversation(modelStore.currentModel, [], '新会话');
|
||||
// create empty conversation on server and navigate
|
||||
const conv = await convStore.createConversation(modelStore.currentModel, []);
|
||||
if (conv && conv._id) {
|
||||
router.push('/chat');
|
||||
// let ChatView load via store currentId watcher
|
||||
@@ -97,7 +97,18 @@ watch(
|
||||
|
||||
if (authStore.isAuthed) convStore.fetchList();
|
||||
|
||||
function avatarUrl(email: string) { return ''; }
|
||||
function avatarUrlForName(name: string) {
|
||||
// simple SVG avatar generator using initials and consistent color based on name
|
||||
const initials = (name || '访客').trim().split(/\s+/).slice(0,2).map(s => s[0]).join('').toUpperCase();
|
||||
// pick color from hash
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
const hue = Math.abs(hash) % 360;
|
||||
const bg = `hsl(${hue} 60% 45%)`;
|
||||
const fg = '#fff';
|
||||
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='80' height='80'><rect width='100%' height='100%' fill='${bg}' rx='16'/><text x='50%' y='52%' dominant-baseline='middle' text-anchor='middle' font-family='system-ui,Segoe UI,Roboto,Helvetica,Arial' font-size='32' fill='${fg}'>${initials}</text></svg>`;
|
||||
return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
|
||||
import axios from 'axios';
|
||||
import { useAuthStore } from './authStore';
|
||||
|
||||
export interface ConversationSummary { _id: string; title: string; modelId: string; updatedAt: string; }
|
||||
export interface ConversationSummary { _id: string; title: string; modelId: string; updatedAt: string; transient?: boolean }
|
||||
export interface ConversationMessage { role: 'user' | 'assistant' | 'system'; content: string; transient?: boolean }
|
||||
|
||||
interface ConversationState {
|
||||
@@ -16,6 +16,16 @@ interface ConversationState {
|
||||
export const useConversationStore = defineStore('conversation', {
|
||||
state: (): ConversationState => ({ list: [], loading: false, error: null, currentId: null, messages: [] }),
|
||||
actions: {
|
||||
ensureInList(conv: any) {
|
||||
if (!conv || !conv._id) return;
|
||||
const idx = this.list.findIndex(l => l._id === conv._id);
|
||||
const item = { _id: conv._id, title: conv.title || '未命名', modelId: conv.modelId || '', updatedAt: conv.updatedAt || new Date().toISOString() } as ConversationSummary;
|
||||
if (idx >= 0) {
|
||||
this.list[idx] = { ...this.list[idx], ...item };
|
||||
} else {
|
||||
this.list.unshift(item);
|
||||
}
|
||||
},
|
||||
async fetchList() {
|
||||
const auth = useAuthStore();
|
||||
if (!auth.token) { this.list = []; return; }
|
||||
@@ -34,26 +44,41 @@ export const useConversationStore = defineStore('conversation', {
|
||||
const { data } = await axios.get('/api/conversations/' + id, { headers: { Authorization: 'Bearer ' + auth.token } });
|
||||
this.currentId = id;
|
||||
this.messages = data.messages || [];
|
||||
// ensure the list contains this conversation (may have been created elsewhere)
|
||||
if (!this.list.find(l => l._id === data._id)) {
|
||||
this.list.unshift({ _id: data._id, title: data.title || '未命名', modelId: data.modelId, updatedAt: data.updatedAt });
|
||||
}
|
||||
} catch (e:any) { this.error = e.response?.data?.error || e.message; }
|
||||
finally { this.loading = false; }
|
||||
},
|
||||
resetCurrent() { this.currentId = null; this.messages = []; },
|
||||
createNewConversation(notify?: string) {
|
||||
this.currentId = null;
|
||||
// create a local transient placeholder conversation so the list updates immediately
|
||||
const tempId = 'local-' + Date.now() + '-' + Math.random().toString(36).slice(2,8);
|
||||
const placeholder: ConversationSummary = { _id: tempId, title: '未命名', modelId: '', updatedAt: new Date().toISOString(), transient: true };
|
||||
// insert placeholder at top
|
||||
this.list.unshift(placeholder);
|
||||
this.currentId = tempId;
|
||||
this.messages = [];
|
||||
if (notify) {
|
||||
this.messages.push({ role: 'system', content: notify, transient: true });
|
||||
}
|
||||
return placeholder;
|
||||
},
|
||||
async createConversation(modelId: string, messages: any[] = [], title?: string) {
|
||||
async createConversation(modelId: string, messages: any[] = [], title?: string) {
|
||||
const auth = useAuthStore();
|
||||
if (!auth.token) return null;
|
||||
try {
|
||||
const { data } = await axios.post('/api/conversations', { modelId, messages, title }, { headers: { Authorization: 'Bearer ' + auth.token } });
|
||||
// prepend to list
|
||||
this.list.unshift(data);
|
||||
this.currentId = data._id;
|
||||
return data;
|
||||
// remove first transient placeholder if present (we'll replace it)
|
||||
const phIndex = this.list.findIndex(l => l.transient);
|
||||
if (phIndex >= 0) this.list.splice(phIndex, 1);
|
||||
this.list.unshift(data);
|
||||
this.currentId = data._id;
|
||||
// load messages immediately so UI has the content
|
||||
await this.loadConversation(data._id);
|
||||
return data;
|
||||
} catch (e:any) { this.error = e.response?.data?.error || e.message; return null; }
|
||||
},
|
||||
pushMessage(m: ConversationMessage) { this.messages.push(m); },
|
||||
|
@@ -10,7 +10,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
port: 5175,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
|
Reference in New Issue
Block a user