Browse Source

feat: enhance mobile responsiveness and UI adjustments across agent components

feat/ai-sdk-v6-upgrade
npmrun 1 day ago
parent
commit
81408be21f
  1. 8
      app/components/agent/AgentInput.vue
  2. 17
      app/components/agent/AgentModelSelector.vue
  3. 123
      app/components/agent/AgentSidebar.vue
  4. 39
      app/components/agent/AgentToolbar.vue
  5. 15
      app/composables/useAgentChat.ts
  6. 22
      app/pages/index.vue
  7. BIN
      packages/drizzle-pkg/db.sqlite

8
app/components/agent/AgentInput.vue

@ -361,14 +361,14 @@ defineExpose({
}
.btn-stop {
width: 36px;
height: 36px;
background: var(--color-surface-dark);
color: var(--color-on-dark);
font-size: 12px;
font-size: 13px;
border-radius: 18px;
padding: 0 12px;
gap: 4px;
padding: 0 14px;
gap: 5px;
white-space: nowrap;
}
.btn-stop:hover {

17
app/components/agent/AgentModelSelector.vue

@ -166,4 +166,21 @@ onUnmounted(() => {
font-size: 13px;
color: var(--color-muted);
}
/* ── Mobile ── */
@media (max-width: 767.98px) {
.selector-trigger {
padding: 6px 8px;
gap: 4px;
}
.trigger-label {
max-width: 90px;
}
.selector-dropdown {
min-width: 200px;
}
}
</style>

123
app/components/agent/AgentSidebar.vue

@ -7,6 +7,7 @@ const props = defineProps<{
loggedIn: boolean;
user: { name?: string | null; email?: string | null; role?: string | null } | null;
collapsed: boolean;
mobile?: boolean;
}>();
const emit = defineEmits<{
@ -16,13 +17,15 @@ const emit = defineEmits<{
delete: [id: string];
login: [];
toggle: [];
close: [];
}>();
const isAdmin = computed(() => props.user?.role === "admin");
</script>
<template>
<aside class="agent-sidebar" :class="{ collapsed }">
<!-- Desktop: inline flex sidebar (hidden on mobile via CSS) -->
<aside class="agent-sidebar desktop" :class="{ collapsed }">
<div class="sidebar-header">
<button class="new-chat-btn" @click="emit('newSession')">
<Icon name="lucide:plus" />
@ -78,6 +81,77 @@ const isAdmin = computed(() => props.user?.role === "admin");
</template>
</div>
</aside>
<!-- Mobile: overlay drawer (hidden on desktop via CSS) -->
<ClientOnly>
<Teleport to="body">
<Transition name="sidebar-overlay">
<div
v-if="mobile && !collapsed"
class="sidebar-overlay"
@click="emit('close')"
/>
</Transition>
<Transition name="sidebar-slide">
<aside v-if="mobile && !collapsed" class="agent-sidebar mobile">
<div class="sidebar-header">
<button class="new-chat-btn" @click="emit('newSession')">
<Icon name="lucide:plus" />
<span>新对话</span>
</button>
<button class="collapse-btn" @click="emit('close')">
<Icon name="lucide:x" />
</button>
</div>
<div class="sidebar-sessions">
<AgentSidebarItem
v-for="s in sessions"
:key="s.id"
:session="s"
:active="s.id === currentSessionId"
@select="emit('select', s.id)"
@rename="(title) => emit('rename', s.id, title)"
@delete="emit('delete', s.id)"
/>
<div v-if="sessions.length === 0" class="empty-sessions">
暂无对话
</div>
</div>
<div class="sidebar-footer">
<div v-if="loggedIn" class="footer-links">
<NuxtLink to="/settings" class="footer-link">
<Icon name="lucide:settings" />
<span>设置</span>
</NuxtLink>
<NuxtLink v-if="isAdmin" to="/admin/dashboard" class="footer-link">
<Icon name="lucide:shield" />
<span>管理后台</span>
</NuxtLink>
</div>
<template v-if="loggedIn && user">
<div class="user-info">
<div class="user-avatar">
{{ (user.name || user.email || "?").charAt(0).toUpperCase() }}
</div>
<div class="user-meta">
<div class="user-name">{{ user.name || "用户" }}</div>
<div class="user-email">{{ user.email }}</div>
</div>
</div>
</template>
<template v-else>
<button class="login-btn" @click="emit('login')">
<Icon name="lucide:log-in" />
<span>登录以保存对话</span>
</button>
</template>
</div>
</aside>
</Transition>
</Teleport>
</ClientOnly>
</template>
<style scoped>
@ -97,6 +171,13 @@ const isAdmin = computed(() => props.user?.role === "admin");
border-right: none;
}
/* Hide desktop sidebar on mobile */
@media (max-width: 767.98px) {
.agent-sidebar.desktop {
display: none;
}
}
.sidebar-header {
display: flex;
align-items: center;
@ -252,4 +333,44 @@ const isAdmin = computed(() => props.user?.role === "admin");
border-color: var(--color-primary);
color: var(--color-primary);
}
/* ── Mobile overlay drawer ── */
.sidebar-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 199;
}
.agent-sidebar.mobile {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 280px;
max-width: 85vw;
z-index: 200;
box-shadow: 2px 0 16px rgba(0, 0, 0, 0.15);
}
.sidebar-overlay-enter-active,
.sidebar-overlay-leave-active {
transition: opacity 0.2s ease;
}
.sidebar-overlay-enter-from,
.sidebar-overlay-leave-to {
opacity: 0;
}
.sidebar-slide-enter-active,
.sidebar-slide-leave-active {
transition: transform 0.25s ease;
}
.sidebar-slide-enter-from,
.sidebar-slide-leave-to {
transform: translateX(-100%);
}
</style>

39
app/components/agent/AgentToolbar.vue

@ -38,7 +38,7 @@ const emit = defineEmits<{
<div class="toolbar-right">
<!-- Rate limit indicator (guest only) -->
<div v-if="!loggedIn && rateLimitInfo" class="rate-limit-info">
<div v-if="!loggedIn && rateLimitInfo" class="rate-limit-info" :data-short="rateLimitInfo.sessionRemaining !== null ? `${rateLimitInfo.sessionRemaining}/20` : ''">
<Icon name="lucide:zap" class="rl-icon" />
<span v-if="rateLimitInfo.sessionRemaining !== null">
本对话 {{ rateLimitInfo.sessionRemaining }}/20
@ -129,14 +129,51 @@ const emit = defineEmits<{
border-radius: 8px;
font-size: 12px;
color: var(--color-muted);
white-space: nowrap;
flex-shrink: 0;
}
.rl-icon {
font-size: 13px;
color: var(--color-accent-amber);
flex-shrink: 0;
}
.rl-sep {
margin: 0 2px;
}
/* ── Mobile ── */
@media (max-width: 767.98px) {
.agent-toolbar {
padding: 10px 12px;
}
.toolbar-left,
.toolbar-right {
gap: 6px;
min-width: 0;
}
.toolbar-left {
flex: 1;
min-width: 0;
}
.rate-limit-info {
padding: 4px 8px;
font-size: 11px;
}
.rate-limit-info .rl-sep,
.rate-limit-info > span:not(.rl-icon) {
display: none;
}
.rate-limit-info::after {
content: attr(data-short);
font-size: 11px;
}
}
</style>

15
app/composables/useAgentChat.ts

@ -513,12 +513,15 @@ export function useAgentChat(options: UseAgentChatOptions) {
const msg = messages.value.find((m) => m.id === messageId);
const newFeedback = msg?.feedback === feedback ? null : feedback;
try {
await $fetch("/api/agent/feedback", {
method: "POST",
body: { messageId, feedback: newFeedback },
});
if (msg) {
msg.feedback = newFeedback;
const res = await $fetch<{ code: number; data: { messageId: string; feedback: "like" | "dislike" | null } }>(
"/api/agent/feedback",
{
method: "POST",
body: { messageId, feedback: newFeedback },
},
);
if (res?.code === 0 && msg) {
msg.feedback = res.data?.feedback ?? newFeedback;
}
} catch {
}

22
app/pages/index.vue

@ -9,7 +9,9 @@ definePageMeta({
const auth = useAuthSession();
const sessions = useAgentSessions();
const isMobile = useMediaQuery('(max-width: 767.98px)');
const sidebarCollapsed = ref(false);
const mobileSidebarOpen = ref(false);
const showPromptModal = ref(false);
const systemPromptText = ref("");
const preferredModelId = ref<number | null>(null);
@ -154,6 +156,14 @@ watch(
},
);
watch(isMobile, (mobile) => {
if (mobile) {
mobileSidebarOpen.value = false;
} else {
sidebarCollapsed.value = false;
}
}, { immediate: true });
async function handleSend(content: string) {
if (!sessions.currentSessionId.value) {
await sessions.newSession();
@ -214,13 +224,15 @@ onMounted(async () => {
:current-session-id="sessions.currentSessionId.value"
:logged-in="auth.loggedIn.value"
:user="auth.user.value"
:collapsed="sidebarCollapsed"
@new-session="sessions.newSession()"
@select="sessions.selectSession($event)"
:collapsed="isMobile ? !mobileSidebarOpen : sidebarCollapsed"
:mobile="isMobile"
@new-session="sessions.newSession(); if (isMobile) mobileSidebarOpen = false"
@select="sessions.selectSession($event); if (isMobile) mobileSidebarOpen = false"
@rename="sessions.renameSession"
@delete="sessions.deleteSession"
@login="handleLogin"
@toggle="sidebarCollapsed = !sidebarCollapsed"
@toggle="isMobile ? (mobileSidebarOpen = !mobileSidebarOpen) : (sidebarCollapsed = !sidebarCollapsed)"
@close="mobileSidebarOpen = false"
/>
<AgentChatArea
@ -244,7 +256,7 @@ onMounted(async () => {
@feedback="handleFeedback"
@edit="handleEdit"
@regenerate="handleRegenerate"
@toggle-sidebar="sidebarCollapsed = !sidebarCollapsed"
@toggle-sidebar="isMobile ? (mobileSidebarOpen = !mobileSidebarOpen) : (sidebarCollapsed = !sidebarCollapsed)"
@show-prompt="showPromptModal = true"
@example="handleExample"
/>

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.
Loading…
Cancel
Save