优化会话列表,优化头像显示
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) => {
|
router.post('/conversations', auth(), async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { modelId, messages, title } = req.body;
|
const { modelId, messages, title } = req.body;
|
||||||
const conv = await Conversation.create({ userId: req.userId, modelId, messages, title });
|
// clean messages: remove system/transient or empty entries
|
||||||
res.json(conv);
|
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) {
|
} catch (e:any) {
|
||||||
res.status(500).json({ error: e.message });
|
res.status(500).json({ error: e.message });
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
<div class="sidebar-bottom">
|
<div class="sidebar-bottom">
|
||||||
<div class="avatar-area left-align" style="position:relative;">
|
<div class="avatar-area left-align" style="position:relative;">
|
||||||
<button class="avatar-btn" @click="authStore.isAuthed ? (showUserMenu = !showUserMenu) : goLogin()">
|
<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>
|
<div v-else class="avatar-fallback">{{ userInitial }}</div>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="showUserMenu" class="user-menu">
|
<div v-if="showUserMenu" class="user-menu">
|
||||||
@@ -69,8 +69,8 @@ function logout() { authStore.logout(); showUserMenu.value = false; router.push(
|
|||||||
function toggleTheme() { themeStore.toggle(); }
|
function toggleTheme() { themeStore.toggle(); }
|
||||||
async function handleNewConv() {
|
async function handleNewConv() {
|
||||||
if (authStore.isAuthed) {
|
if (authStore.isAuthed) {
|
||||||
// create empty conversation on server and navigate
|
// create empty conversation on server and navigate
|
||||||
const conv = await convStore.createConversation(modelStore.currentModel, [], '新会话');
|
const conv = await convStore.createConversation(modelStore.currentModel, []);
|
||||||
if (conv && conv._id) {
|
if (conv && conv._id) {
|
||||||
router.push('/chat');
|
router.push('/chat');
|
||||||
// let ChatView load via store currentId watcher
|
// let ChatView load via store currentId watcher
|
||||||
@@ -97,7 +97,18 @@ watch(
|
|||||||
|
|
||||||
if (authStore.isAuthed) convStore.fetchList();
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useAuthStore } from './authStore';
|
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 }
|
export interface ConversationMessage { role: 'user' | 'assistant' | 'system'; content: string; transient?: boolean }
|
||||||
|
|
||||||
interface ConversationState {
|
interface ConversationState {
|
||||||
@@ -16,6 +16,16 @@ interface ConversationState {
|
|||||||
export const useConversationStore = defineStore('conversation', {
|
export const useConversationStore = defineStore('conversation', {
|
||||||
state: (): ConversationState => ({ list: [], loading: false, error: null, currentId: null, messages: [] }),
|
state: (): ConversationState => ({ list: [], loading: false, error: null, currentId: null, messages: [] }),
|
||||||
actions: {
|
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() {
|
async fetchList() {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
if (!auth.token) { this.list = []; return; }
|
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 } });
|
const { data } = await axios.get('/api/conversations/' + id, { headers: { Authorization: 'Bearer ' + auth.token } });
|
||||||
this.currentId = id;
|
this.currentId = id;
|
||||||
this.messages = data.messages || [];
|
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; }
|
} catch (e:any) { this.error = e.response?.data?.error || e.message; }
|
||||||
finally { this.loading = false; }
|
finally { this.loading = false; }
|
||||||
},
|
},
|
||||||
resetCurrent() { this.currentId = null; this.messages = []; },
|
resetCurrent() { this.currentId = null; this.messages = []; },
|
||||||
createNewConversation(notify?: string) {
|
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 = [];
|
this.messages = [];
|
||||||
if (notify) {
|
if (notify) {
|
||||||
this.messages.push({ role: 'system', content: notify, transient: true });
|
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();
|
const auth = useAuthStore();
|
||||||
if (!auth.token) return null;
|
if (!auth.token) return null;
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.post('/api/conversations', { modelId, messages, title }, { headers: { Authorization: 'Bearer ' + auth.token } });
|
const { data } = await axios.post('/api/conversations', { modelId, messages, title }, { headers: { Authorization: 'Bearer ' + auth.token } });
|
||||||
// prepend to list
|
// prepend to list
|
||||||
this.list.unshift(data);
|
// remove first transient placeholder if present (we'll replace it)
|
||||||
this.currentId = data._id;
|
const phIndex = this.list.findIndex(l => l.transient);
|
||||||
return data;
|
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; }
|
} catch (e:any) { this.error = e.response?.data?.error || e.message; return null; }
|
||||||
},
|
},
|
||||||
pushMessage(m: ConversationMessage) { this.messages.push(m); },
|
pushMessage(m: ConversationMessage) { this.messages.push(m); },
|
||||||
|
@@ -10,7 +10,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5175,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:3000',
|
target: 'http://localhost:3000',
|
||||||
|
Reference in New Issue
Block a user