Merge branch 'dev' into documents-mentions

This commit is contained in:
Thierry CH. 2025-12-24 06:31:49 +02:00 committed by GitHub
commit c4400a0ec2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 55 additions and 115 deletions

View file

@ -6,9 +6,8 @@ Create Date: 2025-12-21
This migration: This migration:
1. Migrates data from old 'chats' table to 'new_chat_threads' and 'new_chat_messages' 1. Migrates data from old 'chats' table to 'new_chat_threads' and 'new_chat_messages'
2. Drops the 'podcasts' table (podcast data is not migrated as per user request) 2. Drops the 'chats' table
3. Drops the 'chats' table 3. Removes the 'chattype' enum
4. Removes the 'chattype' enum
""" """
import json import json
@ -92,7 +91,11 @@ def upgrade() -> None:
print(f"[Migration 49] Skipping empty chat {chat_id}") print(f"[Migration 49] Skipping empty chat {chat_id}")
continue continue
# Create new thread # Create new thread - truncate title to 500 chars (VARCHAR(500) limit)
thread_title = title or "Migrated Chat"
if len(thread_title) > 500:
thread_title = thread_title[:497] + "..."
result = connection.execute( result = connection.execute(
sa.text(""" sa.text("""
INSERT INTO new_chat_threads INSERT INTO new_chat_threads
@ -101,7 +104,7 @@ def upgrade() -> None:
RETURNING id RETURNING id
"""), """),
{ {
"title": title or "Migrated Chat", "title": thread_title,
"search_space_id": search_space_id, "search_space_id": search_space_id,
"created_at": created_at, "created_at": created_at,
}, },
@ -162,11 +165,7 @@ def upgrade() -> None:
print(f"[Migration 49] Successfully migrated {migrated_count} chats") print(f"[Migration 49] Successfully migrated {migrated_count} chats")
# Drop podcasts table (FK references chats, so drop first) # Drop chats table (podcasts table was already updated to remove chat_id FK)
print("[Migration 49] Dropping podcasts table...")
op.drop_table("podcasts")
# Drop chats table
print("[Migration 49] Dropping chats table...") print("[Migration 49] Dropping chats table...")
op.drop_table("chats") op.drop_table("chats")
@ -178,7 +177,7 @@ def upgrade() -> None:
def downgrade() -> None: def downgrade() -> None:
"""Recreate old tables (data cannot be restored).""" """Recreate old chats table (data cannot be restored)."""
# Recreate chattype enum # Recreate chattype enum
op.execute( op.execute(
sa.text(""" sa.text("""
@ -209,32 +208,4 @@ def downgrade() -> None:
), ),
) )
# Recreate podcasts table print("[Migration 49 Downgrade] Chats table recreated (data not restored)")
op.create_table(
"podcasts",
sa.Column("id", sa.Integer(), primary_key=True, index=True),
sa.Column("title", sa.String(), nullable=False, index=True),
sa.Column("podcast_transcript", sa.JSON(), nullable=False, server_default="{}"),
sa.Column("file_location", sa.String(500), nullable=False, server_default=""),
sa.Column(
"chat_id",
sa.Integer(),
sa.ForeignKey("chats.id", ondelete="CASCADE"),
nullable=True,
),
sa.Column("chat_state_version", sa.BigInteger(), nullable=True),
sa.Column(
"search_space_id",
sa.Integer(),
sa.ForeignKey("searchspaces.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column(
"created_at",
sa.TIMESTAMP(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
)
print("[Migration 49 Downgrade] Tables recreated (data not restored)")

View file

@ -6,8 +6,8 @@ Create Date: 2024-12-22
This migration: This migration:
1. Migrates data from old llm_configs table to new_llm_configs (preserving user configs) 1. Migrates data from old llm_configs table to new_llm_configs (preserving user configs)
2. Drops the old llm_configs table (no longer used) 2. Updates searchspaces to point to migrated configs
3. Removes the is_default column from new_llm_configs (roles now determine which config to use) 3. Drops the old llm_configs table (no longer used)
""" """
from alembic import op from alembic import op
@ -47,7 +47,6 @@ def upgrade():
system_instructions, system_instructions,
use_default_system_instructions, use_default_system_instructions,
citations_enabled, citations_enabled,
is_default,
search_space_id, search_space_id,
created_at created_at
) )
@ -59,11 +58,10 @@ def upgrade():
lc.model_name, lc.model_name,
lc.api_key, lc.api_key,
lc.api_base, lc.api_base,
COALESCE(lc.litellm_params, '{}'::jsonb), COALESCE(lc.litellm_params::json, '{}'::json),
'' as system_instructions, -- Use defaults '' as system_instructions, -- Use defaults
TRUE as use_default_system_instructions, TRUE as use_default_system_instructions,
TRUE as citations_enabled, TRUE as citations_enabled,
FALSE as is_default,
lc.search_space_id, lc.search_space_id,
COALESCE(lc.created_at, NOW()) COALESCE(lc.created_at, NOW())
FROM llm_configs lc FROM llm_configs lc
@ -130,23 +128,7 @@ def upgrade():
""" """
) )
# STEP 3: Drop the is_default column from new_llm_configs # STEP 3: Drop the old llm_configs table (data has been migrated)
# (role assignments now determine which config to use)
op.execute(
"""
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'new_llm_configs' AND column_name = 'is_default'
) THEN
ALTER TABLE new_llm_configs DROP COLUMN is_default;
END IF;
END$$;
"""
)
# STEP 4: Drop the old llm_configs table (data has been migrated)
op.execute("DROP TABLE IF EXISTS llm_configs CASCADE") op.execute("DROP TABLE IF EXISTS llm_configs CASCADE")
@ -213,7 +195,7 @@ def downgrade():
nlc.api_key, nlc.api_key,
nlc.api_base, nlc.api_base,
'English' as language, -- Default language 'English' as language, -- Default language
COALESCE(nlc.litellm_params, '{}'::jsonb), COALESCE(nlc.litellm_params::jsonb, '{}'::jsonb),
nlc.search_space_id, nlc.search_space_id,
nlc.created_at nlc.created_at
FROM new_llm_configs nlc FROM new_llm_configs nlc
@ -227,18 +209,3 @@ def downgrade():
END$$; END$$;
""" """
) )
# Add back the is_default column to new_llm_configs
op.execute(
"""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'new_llm_configs' AND column_name = 'is_default'
) THEN
ALTER TABLE new_llm_configs ADD COLUMN is_default BOOLEAN NOT NULL DEFAULT FALSE;
END IF;
END$$;
"""
)

View file

@ -253,7 +253,7 @@ export function DashboardClientLayout({
/> />
<SidebarInset className="h-full "> <SidebarInset className="h-full ">
<main className="flex flex-col h-full"> <main className="flex flex-col h-full">
<header className="sticky top-0 z-50 flex h-16 shrink-0 items-center gap-2 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 border-b"> <header className="sticky top-0 flex h-16 shrink-0 items-center gap-2 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 border-b">
<div className="flex items-center justify-between w-full gap-2 px-4"> <div className="flex items-center justify-between w-full gap-2 px-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" /> <SidebarTrigger className="-ml-1" />
@ -265,7 +265,7 @@ export function DashboardClientLayout({
</div> </div>
</div> </div>
</header> </header>
<div className="grow flex-1 overflow-auto min-h-[calc(100vh-64px)]">{children}</div> <div className="flex-1 overflow-hidden">{children}</div>
</main> </main>
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>

View file

@ -250,7 +250,7 @@ export default function ConnectorsPage() {
}; };
return ( return (
<div className="container mx-auto py-8 max-w-6xl"> <div className="container mx-auto py-8 px-4 max-w-6xl min-h-[calc(100vh-64px)]">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}

View file

@ -189,7 +189,7 @@ export default function DocumentsTable() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="w-full px-6 py-4" className="w-full px-6 py-4 min-h-[calc(100vh-64px)]"
> >
<DocumentsFilters <DocumentsFilters
typeCounts={typeCounts ?? {}} typeCounts={typeCounts ?? {}}

View file

@ -444,7 +444,7 @@ export default function LogsManagePage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="w-full px-6 py-4 space-y-6" className="w-full px-6 py-4 space-y-6 min-h-[calc(100vh-64px)]"
> >
{/* Summary Dashboard */} {/* Summary Dashboard */}
<LogsSummaryDashboard <LogsSummaryDashboard

View file

@ -694,11 +694,11 @@ export default function NewChatPage() {
<LinkPreviewToolUI /> <LinkPreviewToolUI />
<DisplayImageToolUI /> <DisplayImageToolUI />
<ScrapeWebpageToolUI /> <ScrapeWebpageToolUI />
<div className="flex flex-col h-[calc(100vh-64px)] max-h-[calc(100vh-64px)] overflow-hidden"> <div className="flex flex-col h-[calc(100vh-64px)] overflow-hidden">
<ChatHeader searchSpaceId={searchSpaceId} /> <Thread
<div className="flex-1 min-h-0 overflow-hidden"> messageThinkingSteps={messageThinkingSteps}
<Thread messageThinkingSteps={messageThinkingSteps} /> header={<ChatHeader searchSpaceId={searchSpaceId} />}
</div> />
</div> </div>
</AssistantRuntimeProvider> </AssistantRuntimeProvider>
); );

View file

@ -34,7 +34,7 @@ export default function AddSourcesPage() {
}; };
return ( return (
<div className="container mx-auto py-8 px-4"> <div className="container mx-auto py-8 px-4 min-h-[calc(100vh-64px)]">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}

View file

@ -8,6 +8,7 @@ import {
ThreadPrimitive, ThreadPrimitive,
useAssistantState, useAssistantState,
useThreadViewport, useThreadViewport,
useMessage,
} from "@assistant-ui/react"; } from "@assistant-ui/react";
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { import {
@ -75,6 +76,8 @@ import { cn } from "@/lib/utils";
*/ */
interface ThreadProps { interface ThreadProps {
messageThinkingSteps?: Map<string, ThinkingStep[]>; messageThinkingSteps?: Map<string, ThinkingStep[]>;
/** Optional header component to render at the top of the viewport (sticky) */
header?: React.ReactNode;
} }
// Context to pass thinking steps to AssistantMessage // Context to pass thinking steps to AssistantMessage
@ -267,20 +270,21 @@ const ThinkingStepsScrollHandler: FC = () => {
}; };
export const Thread: FC<ThreadProps> = ({ messageThinkingSteps = new Map() }) => { export const Thread: FC<ThreadProps> = ({ messageThinkingSteps = new Map() }) => {
export const Thread: FC<ThreadProps> = ({ messageThinkingSteps = new Map(), header }) => {
return ( return (
<ThinkingStepsContext.Provider value={messageThinkingSteps}> <ThinkingStepsContext.Provider value={messageThinkingSteps}>
<ThreadPrimitive.Root <ThreadPrimitive.Root
className="aui-root aui-thread-root @container flex h-full flex-col bg-background" className="aui-root aui-thread-root @container flex h-full min-h-0 flex-col bg-background"
style={{ style={{
["--thread-max-width" as string]: "44rem", ["--thread-max-width" as string]: "44rem",
}} }}
> >
<ThreadPrimitive.Viewport <ThreadPrimitive.Viewport
turnAnchor="top" turnAnchor="top"
className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4" className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
> >
{/* Auto-scroll handler for thinking steps - must be inside Viewport */} {/* Optional sticky header for model selector etc. */}
<ThinkingStepsScrollHandler /> {header && <div className="sticky top-0 z-10 mb-4">{header}</div>}
<AssistantIf condition={({ thread }) => thread.isEmpty}> <AssistantIf condition={({ thread }) => thread.isEmpty}>
<ThreadWelcome /> <ThreadWelcome />
@ -375,7 +379,7 @@ const ThreadWelcome: FC = () => {
return ( return (
<div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative"> <div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative">
{/* Greeting positioned above the composer - fixed position */} {/* Greeting positioned above the composer - fixed position */}
<div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center z-10"> <div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center">
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-5xl delay-100 duration-500 ease-out fill-mode-both"> <h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-5xl delay-100 duration-500 ease-out fill-mode-both">
{greeting} {greeting}
</h1> </h1>

View file

@ -47,12 +47,7 @@ export function ChatHeader({ searchSpaceId }: ChatHeaderProps) {
return ( return (
<> <>
{/* Header Bar */} <ModelSelector onEdit={handleEditConfig} onAddNew={handleAddNew} />
<div className="flex items-center justify-between px-4 py-2 border-b border-border/30 bg-background/80 backdrop-blur-sm">
<ModelSelector onEdit={handleEditConfig} onAddNew={handleAddNew} />
</div>
{/* Config Sidebar */}
<ModelConfigSidebar <ModelConfigSidebar
open={sidebarOpen} open={sidebarOpen}
onOpenChange={handleSidebarClose} onOpenChange={handleSidebarClose}

View file

@ -175,9 +175,10 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
className={cn( className={cn(
"h-9 gap-2 px-3 rounded-xl border border-border/50 bg-background/50 backdrop-blur-sm", "h-9 gap-2 px-3 rounded-xl border border-border/30 bg-background/50 backdrop-blur-sm",
"hover:bg-muted/80 hover:border-border transition-all duration-200", "hover:bg-muted/80 hover:border-border/30 transition-all duration-200",
"text-sm font-medium text-foreground", "text-sm font-medium text-foreground",
"focus-visible:ring-0 focus-visible:ring-offset-0",
className className
)} )}
> >
@ -206,11 +207,14 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
className="w-[360px] p-0 rounded-xl shadow-lg border-border/50" className="w-[360px] p-0 rounded-xl shadow-lg border-border/30"
align="start" align="start"
sideOffset={8} sideOffset={8}
> >
<Command shouldFilter={false} className="rounded-xl relative"> <Command
shouldFilter={false}
className="rounded-xl relative [&_[data-slot=command-input-wrapper]]:border-0 [&_[data-slot=command-input-wrapper]]:px-0 [&_[data-slot=command-input-wrapper]]:gap-2"
>
{/* Switching overlay */} {/* Switching overlay */}
{isSwitching && ( {isSwitching && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-background/80 backdrop-blur-sm rounded-xl"> <div className="absolute inset-0 z-10 flex items-center justify-center bg-background/80 backdrop-blur-sm rounded-xl">
@ -221,8 +225,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
</div> </div>
)} )}
<div className="flex items-center gap-2 border-b px-3 py-2 bg-muted/30"> <div className="flex items-center gap-2 border-b border-border/30 px-3 py-2">
<Bot className="size-4 text-muted-foreground" />
<CommandInput <CommandInput
placeholder="Search models..." placeholder="Search models..."
value={searchQuery} value={searchQuery}
@ -300,7 +303,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
)} )}
{filteredGlobalConfigs.length > 0 && filteredUserConfigs.length > 0 && ( {filteredGlobalConfigs.length > 0 && filteredUserConfigs.length > 0 && (
<CommandSeparator className="my-1" /> <CommandSeparator className="my-1 bg-border/30" />
)} )}
{/* User Configs Section */} {/* User Configs Section */}
@ -362,7 +365,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
)} )}
{/* Add New Config Button */} {/* Add New Config Button */}
<div className="p-2 border-t border-border/50 bg-muted/20"> <div className="p-2 bg-muted/20">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"

View file

@ -160,8 +160,8 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
return ( return (
<Sheet open={open} onOpenChange={onOpenChange}> <Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="left" className="w-80 p-0 flex flex-col"> <SheetContent side="left" className="w-80 p-0 flex flex-col border-0">
<SheetHeader className="mx-3 px-4 py-4 border-b space-y-3"> <SheetHeader className="mx-3 px-4 pt-4 pb-0 space-y-2">
<SheetTitle>{t("all_chats") || "All Chats"}</SheetTitle> <SheetTitle>{t("all_chats") || "All Chats"}</SheetTitle>
<SheetDescription className="sr-only"> <SheetDescription className="sr-only">
{t("all_chats_description") || "Browse and manage all your chats"} {t("all_chats_description") || "Browse and manage all your chats"}
@ -175,7 +175,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
placeholder={t("search_chats") || "Search chats..."} placeholder={t("search_chats") || "Search chats..."}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-8 h-9" className="pl-9 pr-8 h-9 border-0 focus-visible:ring-0 focus-visible:border-0 shadow-none"
/> />
{searchQuery && ( {searchQuery && (
<Button <Button
@ -193,7 +193,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
{/* Tab toggle for active/archived (only show when not searching) */} {/* Tab toggle for active/archived (only show when not searching) */}
{!isSearchMode && ( {!isSearchMode && (
<div className="flex border-b mx-3"> <div className="flex border-b mx-3 -mt-3">
<button <button
type="button" type="button"
onClick={() => setShowArchived(false)} onClick={() => setShowArchived(false)}

View file

@ -159,8 +159,8 @@ export function AllNotesSidebar({
return ( return (
<Sheet open={open} onOpenChange={onOpenChange}> <Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="left" className="w-80 p-0 flex flex-col"> <SheetContent side="left" className="w-80 p-0 flex flex-col border-0">
<SheetHeader className="mx-3 px-4 py-4 border-b space-y-3"> <SheetHeader className="mx-3 px-4 pt-4 pb-2 border-b space-y-2">
<SheetTitle>{t("all_notes") || "All Notes"}</SheetTitle> <SheetTitle>{t("all_notes") || "All Notes"}</SheetTitle>
<SheetDescription className="sr-only"> <SheetDescription className="sr-only">
{t("all_notes_description") || "Browse and manage all your notes"} {t("all_notes_description") || "Browse and manage all your notes"}
@ -174,7 +174,7 @@ export function AllNotesSidebar({
placeholder={t("search_notes") || "Search notes..."} placeholder={t("search_notes") || "Search notes..."}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-8 h-9" className="pl-9 pr-8 h-9 border-0 focus-visible:ring-0 focus-visible:border-0 shadow-none"
/> />
{searchQuery && ( {searchQuery && (
<Button <Button