chore: ran linting

This commit is contained in:
Anish Sarkar 2026-03-17 04:40:46 +05:30
parent bc1f31b481
commit ac0f2fa2eb
33 changed files with 688 additions and 661 deletions

View file

@ -262,7 +262,11 @@ def _normalize_connectors(
valid_set -= _LIVE_SEARCH_CONNECTORS valid_set -= _LIVE_SEARCH_CONNECTORS
if not connectors_to_search: if not connectors_to_search:
base = list(available_connectors) if available_connectors else list(_ALL_CONNECTORS) base = (
list(available_connectors)
if available_connectors
else list(_ALL_CONNECTORS)
)
return [c for c in base if c not in _LIVE_SEARCH_CONNECTORS] return [c for c in base if c not in _LIVE_SEARCH_CONNECTORS]
normalized: list[str] = [] normalized: list[str] = []
@ -291,7 +295,11 @@ def _normalize_connectors(
# Fallback to all available if nothing matched # Fallback to all available if nothing matched
if not out: if not out:
base = list(available_connectors) if available_connectors else list(_ALL_CONNECTORS) base = (
list(available_connectors)
if available_connectors
else list(_ALL_CONNECTORS)
)
return [c for c in base if c not in _LIVE_SEARCH_CONNECTORS] return [c for c in base if c not in _LIVE_SEARCH_CONNECTORS]
return out return out

View file

@ -68,12 +68,12 @@ from .podcast import create_generate_podcast_tool
from .report import create_generate_report_tool from .report import create_generate_report_tool
from .scrape_webpage import create_scrape_webpage_tool from .scrape_webpage import create_scrape_webpage_tool
from .search_surfsense_docs import create_search_surfsense_docs_tool from .search_surfsense_docs import create_search_surfsense_docs_tool
from .web_search import create_web_search_tool
from .shared_memory import ( from .shared_memory import (
create_recall_shared_memory_tool, create_recall_shared_memory_tool,
create_save_shared_memory_tool, create_save_shared_memory_tool,
) )
from .user_memory import create_recall_memory_tool, create_save_memory_tool from .user_memory import create_recall_memory_tool, create_save_memory_tool
from .web_search import create_web_search_tool
# ============================================================================= # =============================================================================
# Tool Definition # Tool Definition

View file

@ -72,20 +72,22 @@ def _format_web_results(
continue continue
metadata_json = json.dumps(metadata, ensure_ascii=False) metadata_json = json.dumps(metadata, ensure_ascii=False)
doc_xml = "\n".join([ doc_xml = "\n".join(
"<document>", [
"<document_metadata>", "<document>",
f" <document_type>{source}</document_type>", "<document_metadata>",
f" <title><![CDATA[{title}]]></title>", f" <document_type>{source}</document_type>",
f" <url><![CDATA[{url}]]></url>", f" <title><![CDATA[{title}]]></title>",
f" <metadata_json><![CDATA[{metadata_json}]]></metadata_json>", f" <url><![CDATA[{url}]]></url>",
"</document_metadata>", f" <metadata_json><![CDATA[{metadata_json}]]></metadata_json>",
"<document_content>", "</document_metadata>",
f" <chunk id='{url}'><![CDATA[{content}]]></chunk>", "<document_content>",
"</document_content>", f" <chunk id='{url}'><![CDATA[{content}]]></chunk>",
"</document>", "</document_content>",
"", "</document>",
]) "",
]
)
if total_chars + len(doc_xml) > max_chars: if total_chars + len(doc_xml) > max_chars:
parts.append("<!-- Output truncated to fit context window -->") parts.append("<!-- Output truncated to fit context window -->")
@ -152,9 +154,7 @@ def create_web_search_tool(
] ]
engine_names = ["SearXNG (platform default)"] engine_names = ["SearXNG (platform default)"]
engine_names.extend( engine_names.extend(_CONNECTOR_LABELS.get(c, c) for c in active_live_connectors)
_CONNECTOR_LABELS.get(c, c) for c in active_live_connectors
)
engines_summary = ", ".join(engine_names) engines_summary = ", ".join(engine_names)
description = ( description = (
@ -179,10 +179,12 @@ def create_web_search_tool(
tasks: list[asyncio.Task[list[dict[str, Any]]]] = [] tasks: list[asyncio.Task[list[dict[str, Any]]]] = []
if web_search_service.is_available(): if web_search_service.is_available():
async def _searxng() -> list[dict[str, Any]]: async def _searxng() -> list[dict[str, Any]]:
async with semaphore: async with semaphore:
_result_obj, docs = await web_search_service.search( _result_obj, docs = await web_search_service.search(
query=query, top_k=clamped_top_k, query=query,
top_k=clamped_top_k,
) )
return docs return docs

View file

@ -428,4 +428,4 @@ class ChucksHybridSearchRetriever:
search_space_id, search_space_id,
document_type, document_type,
) )
return final_docs return final_docs

View file

@ -2,7 +2,6 @@ import asyncio
import time import time
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from urllib.parse import urljoin
import httpx import httpx
from linkup import LinkupClient from linkup import LinkupClient

View file

@ -278,7 +278,7 @@ async def get_search_space_llm_instance(
"ALIBABA_QWEN": "openai", "ALIBABA_QWEN": "openai",
"MOONSHOT": "openai", "MOONSHOT": "openai",
"ZHIPU": "openai", "ZHIPU": "openai",
"MINIMAX": "openai", "MINIMAX": "openai",
} }
provider_prefix = provider_map.get( provider_prefix = provider_map.get(
global_config["provider"], global_config["provider"].lower() global_config["provider"], global_config["provider"].lower()

View file

@ -8,6 +8,7 @@ latency overhead.
from __future__ import annotations from __future__ import annotations
import contextlib
import hashlib import hashlib
import json import json
import logging import logging
@ -112,10 +113,8 @@ def _cache_get(key: str) -> dict | None:
def _cache_set(key: str, value: dict) -> None: def _cache_set(key: str, value: dict) -> None:
try: with contextlib.suppress(redis.RedisError):
_get_redis().setex(key, _CACHE_TTL_SECONDS, json.dumps(value)) _get_redis().setex(key, _CACHE_TTL_SECONDS, json.dumps(value))
except redis.RedisError:
pass
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -208,7 +207,9 @@ async def search(
try: try:
async with httpx.AsyncClient(timeout=15.0, verify=False) as client: async with httpx.AsyncClient(timeout=15.0, verify=False) as client:
response = await client.get( response = await client.get(
searx_endpoint, params=params, headers=headers, searx_endpoint,
params=params,
headers=headers,
) )
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
@ -217,7 +218,10 @@ async def search(
last_error = exc last_error = exc
if attempt == 0 and ( if attempt == 0 and (
isinstance(exc, httpx.TimeoutException) isinstance(exc, httpx.TimeoutException)
or (isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code >= 500) or (
isinstance(exc, httpx.HTTPStatusError)
and exc.response.status_code >= 500
)
): ):
continue continue
break break
@ -246,29 +250,33 @@ async def search(
source_id = 200_000 + idx source_id = 200_000 + idx
description = result.get("content") or result.get("snippet") or "" description = result.get("content") or result.get("snippet") or ""
sources_list.append({ sources_list.append(
"id": source_id, {
"title": result.get("title", "Web Search Result"),
"description": description,
"url": result.get("url", ""),
})
documents.append({
"chunk_id": source_id,
"content": description or result.get("content", ""),
"score": result.get("score", 0.0),
"document": {
"id": source_id, "id": source_id,
"title": result.get("title", "Web Search Result"), "title": result.get("title", "Web Search Result"),
"document_type": "SEARXNG_API", "description": description,
"metadata": { "url": result.get("url", ""),
"url": result.get("url", ""), }
"engines": result.get("engines", []), )
"category": result.get("category"),
"source": "SEARXNG_API", documents.append(
{
"chunk_id": source_id,
"content": description or result.get("content", ""),
"score": result.get("score", 0.0),
"document": {
"id": source_id,
"title": result.get("title", "Web Search Result"),
"document_type": "SEARXNG_API",
"metadata": {
"url": result.get("url", ""),
"engines": result.get("engines", []),
"category": result.get("category"),
"source": "SEARXNG_API",
},
}, },
}, }
}) )
result_object: dict[str, Any] = { result_object: dict[str, Any] = {
"id": 11, "id": 11,

View file

@ -51,9 +51,7 @@ async def safe_set_chunks(
from app.db import Chunk from app.db import Chunk
if document.id is not None: if document.id is not None:
await session.execute( await session.execute(delete(Chunk).where(Chunk.document_id == document.id))
delete(Chunk).where(Chunk.document_id == document.id)
)
for chunk in chunks: for chunk in chunks:
chunk.document_id = document.id chunk.document_id = document.id

View file

@ -37,9 +37,7 @@ async def safe_set_chunks(
from app.db import Chunk from app.db import Chunk
if document.id is not None: if document.id is not None:
await session.execute( await session.execute(delete(Chunk).where(Chunk.document_id == document.id))
delete(Chunk).where(Chunk.document_id == document.id)
)
for chunk in chunks: for chunk in chunks:
chunk.document_id = document.id chunk.document_id = document.id

View file

@ -9,11 +9,9 @@ import re
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from sqlalchemy import select from sqlalchemy import delete as sa_delete, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from sqlalchemy import delete as sa_delete
from sqlalchemy.orm.attributes import set_committed_value from sqlalchemy.orm.attributes import set_committed_value
from app.config import config from app.config import config
@ -39,6 +37,7 @@ async def _safe_set_docs_chunks(
set_committed_value(document, "chunks", chunks) set_committed_value(document, "chunks", chunks)
session.add_all(chunks) session.add_all(chunks)
# Path to docs relative to project root # Path to docs relative to project root
DOCS_DIR = ( DOCS_DIR = (
Path(__file__).resolve().parent.parent.parent.parent Path(__file__).resolve().parent.parent.parent.parent

View file

@ -261,9 +261,7 @@ export default function OnboardPage() {
You can add more configurations and customize settings anytime in{" "} You can add more configurations and customize settings anytime in{" "}
<button <button
type="button" type="button"
onClick={() => onClick={() => setSearchSpaceSettingsDialog({ open: true, initialTab: "general" })}
setSearchSpaceSettingsDialog({ open: true, initialTab: "general" })
}
className="text-violet-500 hover:underline" className="text-violet-500 hover:underline"
> >
Settings Settings

View file

@ -657,14 +657,14 @@ function CreateInviteDialog({
return ( return (
<Dialog open={open} onOpenChange={(v) => (v ? setOpen(true) : handleClose())}> <Dialog open={open} onOpenChange={(v) => (v ? setOpen(true) : handleClose())}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="gap-1.5 md:gap-2 text-xs md:text-sm bg-black text-white dark:bg-white dark:text-black hover:bg-black/90 dark:hover:bg-white/90" className="gap-1.5 md:gap-2 text-xs md:text-sm bg-black text-white dark:bg-white dark:text-black hover:bg-black/90 dark:hover:bg-white/90"
> >
<UserPlus className="h-3.5 w-3.5 md:h-4 md:w-4" /> <UserPlus className="h-3.5 w-3.5 md:h-4 md:w-4" />
Invite members Invite members
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
className="w-[92vw] max-w-[92vw] sm:max-w-md p-4 md:p-6 select-none" className="w-[92vw] max-w-[92vw] sm:max-w-md p-4 md:p-6 select-none"
@ -832,13 +832,13 @@ function AllInvitesDialog({
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="secondary" size="sm" className="gap-1.5 md:gap-2 text-xs md:text-sm"> <Button variant="secondary" size="sm" className="gap-1.5 md:gap-2 text-xs md:text-sm">
<Link2 className="h-3.5 w-3.5 md:h-4 md:w-4 rotate-315" /> <Link2 className="h-3.5 w-3.5 md:h-4 md:w-4 rotate-315" />
Active invites Active invites
<span className="inline-flex items-center justify-center h-4 md:h-5 min-w-4 md:min-w-5 px-1 rounded-full bg-neutral-700 text-neutral-200 text-[10px] md:text-xs font-medium"> <span className="inline-flex items-center justify-center h-4 md:h-5 min-w-4 md:min-w-5 px-1 rounded-full bg-neutral-700 text-neutral-200 text-[10px] md:text-xs font-medium">
{invites.length} {invites.length}
</span> </span>
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-lg p-4 md:p-6 select-none"> <DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-lg p-4 md:p-6 select-none">
<DialogHeader> <DialogHeader>

View file

@ -27,58 +27,24 @@ export function ApiKeyContent() {
return ( return (
<div className="space-y-6 min-w-0 overflow-hidden"> <div className="space-y-6 min-w-0 overflow-hidden">
<Alert className="border-border/60 bg-muted/30 text-muted-foreground"> <Alert className="border-border/60 bg-muted/30 text-muted-foreground">
<Info className="h-4 w-4 text-muted-foreground" /> <Info className="h-4 w-4 text-muted-foreground" />
<AlertTitle className="text-muted-foreground">{t("api_key_warning_title")}</AlertTitle> <AlertTitle className="text-muted-foreground">{t("api_key_warning_title")}</AlertTitle>
<AlertDescription className="text-muted-foreground/60"> <AlertDescription className="text-muted-foreground/60">
{t("api_key_warning_description")} {t("api_key_warning_description")}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<div className="rounded-lg border border-border/60 bg-card p-6 min-w-0 overflow-hidden"> <div className="rounded-lg border border-border/60 bg-card p-6 min-w-0 overflow-hidden">
<h3 className="mb-4 text-sm font-semibold tracking-tight">{t("your_api_key")}</h3> <h3 className="mb-4 text-sm font-semibold tracking-tight">{t("your_api_key")}</h3>
{isLoading ? ( {isLoading ? (
<div className="h-12 w-full animate-pulse rounded-md border border-border/60 bg-muted/30" /> <div className="h-12 w-full animate-pulse rounded-md border border-border/60 bg-muted/30" />
) : apiKey ? ( ) : apiKey ? (
<div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5">
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide">
<p className="font-mono text-[10px] text-muted-foreground whitespace-nowrap select-all cursor-text">
{apiKey}
</p>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={copyToClipboard}
className="h-6 w-6 shrink-0 text-muted-foreground hover:text-foreground"
>
{copied ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>{copied ? t("copied") : t("copy")}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
) : (
<p className="text-center text-muted-foreground/60">{t("no_api_key")}</p>
)}
</div>
<div className="rounded-lg border border-border/60 bg-card p-6 min-w-0 overflow-hidden">
<h3 className="mb-2 text-sm font-semibold tracking-tight">{t("usage_title")}</h3>
<p className="mb-4 text-[11px] text-muted-foreground/60">{t("usage_description")}</p>
<div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5"> <div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5">
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide"> <div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide">
<pre className="font-mono text-[10px] text-muted-foreground whitespace-nowrap select-all cursor-text"> <p className="font-mono text-[10px] text-muted-foreground whitespace-nowrap select-all cursor-text">
<code>Authorization: Bearer {apiKey || "YOUR_API_KEY"}</code> {apiKey}
</pre> </p>
</div> </div>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
@ -86,21 +52,55 @@ export function ApiKeyContent() {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={copyUsageToClipboard} onClick={copyToClipboard}
className="h-6 w-6 shrink-0 text-muted-foreground hover:text-foreground" className="h-6 w-6 shrink-0 text-muted-foreground hover:text-foreground"
> >
{copiedUsage ? ( {copied ? (
<Check className="h-3 w-3 text-green-500" /> <Check className="h-3 w-3 text-green-500" />
) : ( ) : (
<Copy className="h-3 w-3" /> <Copy className="h-3 w-3" />
)} )}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{copiedUsage ? t("copied") : t("copy")}</TooltipContent> <TooltipContent>{copied ? t("copied") : t("copy")}</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</div> </div>
) : (
<p className="text-center text-muted-foreground/60">{t("no_api_key")}</p>
)}
</div>
<div className="rounded-lg border border-border/60 bg-card p-6 min-w-0 overflow-hidden">
<h3 className="mb-2 text-sm font-semibold tracking-tight">{t("usage_title")}</h3>
<p className="mb-4 text-[11px] text-muted-foreground/60">{t("usage_description")}</p>
<div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5">
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide">
<pre className="font-mono text-[10px] text-muted-foreground whitespace-nowrap select-all cursor-text">
<code>Authorization: Bearer {apiKey || "YOUR_API_KEY"}</code>
</pre>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={copyUsageToClipboard}
className="h-6 w-6 shrink-0 text-muted-foreground hover:text-foreground"
>
{copiedUsage ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>{copiedUsage ? t("copied") : t("copy")}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
</div> </div>
</div>
); );
} }

View file

@ -72,42 +72,42 @@ export function ProfileContent() {
return ( return (
<div> <div>
{isUserLoading ? ( {isUserLoading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Spinner size="md" className="text-muted-foreground" /> <Spinner size="md" className="text-muted-foreground" />
</div> </div>
) : ( ) : (
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div className="rounded-lg border bg-card p-6"> <div className="rounded-lg border bg-card p-6">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("profile_avatar")}</Label> <Label>{t("profile_avatar")}</Label>
<AvatarDisplay <AvatarDisplay
url={user?.avatar_url || undefined} url={user?.avatar_url || undefined}
fallback={getInitials(user?.email || "")} fallback={getInitials(user?.email || "")}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="display-name">{t("profile_display_name")}</Label> <Label htmlFor="display-name">{t("profile_display_name")}</Label>
<Input <Input
id="display-name" id="display-name"
type="text" type="text"
placeholder={user?.email?.split("@")[0]} placeholder={user?.email?.split("@")[0]}
value={displayName} value={displayName}
onChange={(e) => setDisplayName(e.target.value)} onChange={(e) => setDisplayName(e.target.value)}
/> />
<p className="text-xs text-muted-foreground">{t("profile_display_name_hint")}</p> <p className="text-xs text-muted-foreground">{t("profile_display_name_hint")}</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("profile_email")}</Label> <Label>{t("profile_email")}</Label>
<Input type="email" value={user?.email || ""} disabled /> <Input type="email" value={user?.email || ""} disabled />
</div>
</div> </div>
</div> </div>
</div>
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button
type="submit" type="submit"
variant="outline" variant="outline"
@ -118,8 +118,8 @@ export function ProfileContent() {
{t("profile_save")} {t("profile_save")}
</Button> </Button>
</div> </div>
</form> </form>
)} )}
</div> </div>
); );
} }

View file

@ -419,19 +419,19 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
: "You need to configure a Document Summary LLM before adding connectors. This LLM is used to process and summarize documents from your connected sources."} : "You need to configure a Document Summary LLM before adding connectors. This LLM is used to process and summarize documents from your connected sources."}
</p> </p>
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => { onClick={() => {
handleOpenChange(false); handleOpenChange(false);
setSearchSpaceSettingsDialog({ setSearchSpaceSettingsDialog({
open: true, open: true,
initialTab: "models", initialTab: "models",
}); });
}} }}
> >
<Settings className="mr-2 h-4 w-4" /> <Settings className="mr-2 h-4 w-4" />
Go to Settings Go to Settings
</Button> </Button>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}

View file

@ -159,19 +159,19 @@ const DocumentUploadPopupContent: FC<{
: "You need to configure a Document Summary LLM before uploading files. This LLM is used to process and summarize your uploaded documents."} : "You need to configure a Document Summary LLM before uploading files. This LLM is used to process and summarize your uploaded documents."}
</p> </p>
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => { onClick={() => {
onOpenChange(false); onOpenChange(false);
setSearchSpaceSettingsDialog({ setSearchSpaceSettingsDialog({
open: true, open: true,
initialTab: "models", initialTab: "models",
}); });
}} }}
> >
<Settings className="mr-2 h-4 w-4" /> <Settings className="mr-2 h-4 w-4" />
Go to Settings Go to Settings
</Button> </Button>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
) : ( ) : (

View file

@ -233,7 +233,9 @@ const ThreadWelcome: FC = () => {
<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 */} {/* Greeting positioned above the composer */}
<div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center"> <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 text-3xl md:text-5xl select-none">{greeting}</h1> <h1 className="aui-thread-welcome-message-inner text-3xl md:text-5xl select-none">
{greeting}
</h1>
</div> </div>
{/* Composer - top edge fixed, expands downward only */} {/* Composer - top edge fixed, expands downward only */}
<div className="w-full flex items-start justify-center absolute top-[calc(50%-3.5rem)] left-0 right-0"> <div className="w-full flex items-start justify-center absolute top-[calc(50%-3.5rem)] left-0 right-0">
@ -283,7 +285,11 @@ const ConnectToolsBanner: FC = () => {
<span className="text-[13px] text-muted-foreground/80 flex-1">Connect your tools</span> <span className="text-[13px] text-muted-foreground/80 flex-1">Connect your tools</span>
<AvatarGroup className="shrink-0"> <AvatarGroup className="shrink-0">
{BANNER_CONNECTORS.map(({ type }, i) => ( {BANNER_CONNECTORS.map(({ type }, i) => (
<Avatar key={type} className="size-6" style={{ zIndex: BANNER_CONNECTORS.length - i }}> <Avatar
key={type}
className="size-6"
style={{ zIndex: BANNER_CONNECTORS.length - i }}
>
<AvatarFallback className="bg-muted text-[10px]"> <AvatarFallback className="bg-muted text-[10px]">
{getConnectorIcon(type, "size-3.5")} {getConnectorIcon(type, "size-3.5")}
</AvatarFallback> </AvatarFallback>
@ -601,7 +607,10 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
); );
const filteredEnabledCount = useMemo(() => { const filteredEnabledCount = useMemo(() => {
if (!filteredTools) return 0; if (!filteredTools) return 0;
return filteredTools.length - disabledTools.filter((d) => filteredTools.some((t) => t.name === d)).length; return (
filteredTools.length -
disabledTools.filter((d) => filteredTools.some((t) => t.name === d)).length
);
}, [filteredTools, disabledTools]); }, [filteredTools, disabledTools]);
useEffect(() => { useEffect(() => {
@ -775,8 +784,15 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
)} )}
> >
<motion.div <motion.div
animate={{ rotate: isWebSearchEnabled ? 360 : 0, scale: isWebSearchEnabled ? 1.1 : 1 }} animate={{
whileHover={{ rotate: isWebSearchEnabled ? 360 : 15, scale: 1.1, transition: { type: "spring", stiffness: 300, damping: 10 } }} rotate: isWebSearchEnabled ? 360 : 0,
scale: isWebSearchEnabled ? 1.1 : 1,
}}
whileHover={{
rotate: isWebSearchEnabled ? 360 : 15,
scale: 1.1,
transition: { type: "spring", stiffness: 300, damping: 10 },
}}
transition={{ type: "spring", stiffness: 260, damping: 25 }} transition={{ type: "spring", stiffness: 260, damping: 25 }}
> >
<Globe className="size-4" /> <Globe className="size-4" />

View file

@ -286,9 +286,7 @@ export function LayoutShell({
<div <div
className={cn( className={cn(
"relative hidden md:flex shrink-0 border bg-sidebar z-20 transition-[border-radius,border-color] duration-200", "relative hidden md:flex shrink-0 border bg-sidebar z-20 transition-[border-radius,border-color] duration-200",
anySlideOutOpen anySlideOutOpen ? "rounded-l-xl border-r-0 delay-0" : "rounded-xl delay-150"
? "rounded-l-xl border-r-0 delay-0"
: "rounded-xl delay-150"
)} )}
> >
<Sidebar <Sidebar

View file

@ -335,7 +335,10 @@ export function AllPrivateChatsSidebar({
{isLoading ? ( {isLoading ? (
<div className="space-y-1"> <div className="space-y-1">
{[75, 90, 55, 80, 65, 85].map((titleWidth) => ( {[75, 90, 55, 80, 65, 85].map((titleWidth) => (
<div key={`skeleton-${titleWidth}`} className="flex items-center gap-2 rounded-md px-2 py-1.5"> <div
key={`skeleton-${titleWidth}`}
className="flex items-center gap-2 rounded-md px-2 py-1.5"
>
<Skeleton className="h-4 w-4 shrink-0 rounded" /> <Skeleton className="h-4 w-4 shrink-0 rounded" />
<Skeleton className="h-4 rounded" style={{ width: `${titleWidth}%` }} /> <Skeleton className="h-4 rounded" style={{ width: `${titleWidth}%` }} />
</div> </div>

View file

@ -335,7 +335,10 @@ export function AllSharedChatsSidebar({
{isLoading ? ( {isLoading ? (
<div className="space-y-1"> <div className="space-y-1">
{[75, 90, 55, 80, 65, 85].map((titleWidth) => ( {[75, 90, 55, 80, 65, 85].map((titleWidth) => (
<div key={`skeleton-${titleWidth}`} className="flex items-center gap-2 rounded-md px-2 py-1.5"> <div
key={`skeleton-${titleWidth}`}
className="flex items-center gap-2 rounded-md px-2 py-1.5"
>
<Skeleton className="h-4 w-4 shrink-0 rounded" /> <Skeleton className="h-4 w-4 shrink-0 rounded" />
<Skeleton className="h-4 rounded" style={{ width: `${titleWidth}%` }} /> <Skeleton className="h-4 rounded" style={{ width: `${titleWidth}%` }} />
</div> </div>

View file

@ -73,10 +73,10 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti
const Icon = item.icon; const Icon = item.icon;
const indicator = item.statusIndicator; const indicator = item.statusIndicator;
const joyrideAttr = const joyrideAttr =
item.title === "Inbox" || item.title.toLowerCase().includes("inbox") item.title === "Inbox" || item.title.toLowerCase().includes("inbox")
? { "data-joyride": "inbox-sidebar" } ? { "data-joyride": "inbox-sidebar" }
: {}; : {};
if (isCollapsed) { if (isCollapsed) {
return ( return (

View file

@ -39,30 +39,34 @@ export function SidebarSection({
className className
)} )}
> >
<div className="flex items-center group/section shrink-0 px-2 py-1.5"> <div className="flex items-center group/section shrink-0 px-2 py-1.5">
<CollapsibleTrigger className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors min-w-0"> <CollapsibleTrigger className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors min-w-0">
<span className="truncate">{title}</span> <span className="truncate">{title}</span>
<ChevronRight <ChevronRight
className={cn( className={cn(
"h-3.5 w-3.5 shrink-0 transition-transform duration-200", "h-3.5 w-3.5 shrink-0 transition-transform duration-200",
isOpen && "rotate-90" isOpen && "rotate-90"
)} )}
/> />
</CollapsibleTrigger> </CollapsibleTrigger>
{action && ( {action && (
<div className={cn( <div
"transition-opacity ml-1.5 flex items-center", className={cn(
alwaysShowAction ? "opacity-100" : "opacity-100 md:opacity-0 md:group-hover/section:opacity-100" "transition-opacity ml-1.5 flex items-center",
)}> alwaysShowAction
{action} ? "opacity-100"
</div> : "opacity-100 md:opacity-0 md:group-hover/section:opacity-100"
)} )}
>
{action}
</div>
)}
{persistentAction && ( {persistentAction && (
<div className="shrink-0 ml-auto flex items-center">{persistentAction}</div> <div className="shrink-0 ml-auto flex items-center">{persistentAction}</div>
)} )}
</div> </div>
<CollapsibleContent className={cn("overflow-hidden flex-1 flex flex-col min-h-0")}> <CollapsibleContent className={cn("overflow-hidden flex-1 flex flex-col min-h-0")}>
<div className={cn("px-2 pb-2 flex-1 flex flex-col min-h-0 overflow-hidden")}> <div className={cn("px-2 pb-2 flex-1 flex flex-col min-h-0 overflow-hidden")}>

View file

@ -4,7 +4,6 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import { Earth, User, Users } from "lucide-react"; import { Earth, User, Users } from "lucide-react";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { currentThreadAtom, setThreadVisibilityAtom } from "@/atoms/chat/current-thread.atom"; import { currentThreadAtom, setThreadVisibilityAtom } from "@/atoms/chat/current-thread.atom";

View file

@ -237,7 +237,10 @@ export function ModelSelector({
size="sm" size="sm"
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
className={cn("h-8 gap-2 px-3 text-sm bg-main-panel hover:bg-accent/50 dark:hover:bg-white/[0.06] border border-border/40 select-none", className)} className={cn(
"h-8 gap-2 px-3 text-sm bg-main-panel hover:bg-accent/50 dark:hover:bg-white/[0.06] border border-border/40 select-none",
className
)}
> >
{isLoading ? ( {isLoading ? (
<> <>
@ -281,9 +284,7 @@ export function ModelSelector({
)} )}
</> </>
)} )}
<ChevronDown <ChevronDown className="h-3.5 w-3.5 text-muted-foreground ml-1 shrink-0" />
className="h-3.5 w-3.5 text-muted-foreground ml-1 shrink-0"
/>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>

View file

@ -58,52 +58,52 @@ export function PublicChatSnapshotRow({
<Card className="group relative overflow-hidden transition-all duration-200 border-border/60 hover:shadow-md h-full"> <Card className="group relative overflow-hidden transition-all duration-200 border-border/60 hover:shadow-md h-full">
<CardContent className="p-4 flex flex-col gap-3 h-full"> <CardContent className="p-4 flex flex-col gap-3 h-full">
{/* Header: Title + Actions */} {/* Header: Title + Actions */}
<div className="relative"> <div className="relative">
<div className="min-w-0 pr-16 sm:pr-0 sm:group-hover:pr-16"> <div className="min-w-0 pr-16 sm:pr-0 sm:group-hover:pr-16">
<h4 <h4
className="text-sm font-semibold tracking-tight truncate" className="text-sm font-semibold tracking-tight truncate"
title={snapshot.thread_title} title={snapshot.thread_title}
> >
{snapshot.thread_title} {snapshot.thread_title}
</h4> </h4>
</div> </div>
<div className="flex items-center gap-0.5 shrink-0 sm:hidden sm:group-hover:flex absolute right-0 top-0"> <div className="flex items-center gap-0.5 shrink-0 sm:hidden sm:group-hover:flex absolute right-0 top-0">
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
asChild
className="h-7 w-7 text-muted-foreground hover:text-foreground"
>
<a href={snapshot.public_url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-3 w-3" />
</a>
</Button>
</TooltipTrigger>
<TooltipContent>Open link</TooltipContent>
</Tooltip>
</TooltipProvider>
{canDelete && (
<TooltipProvider> <TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}> <Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => onDelete(snapshot)} asChild
disabled={isDeleting} className="h-7 w-7 text-muted-foreground hover:text-foreground"
className="h-7 w-7 text-muted-foreground hover:text-destructive"
> >
<Trash2 className="h-3 w-3" /> <a href={snapshot.public_url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-3 w-3" />
</a>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Delete</TooltipContent> <TooltipContent>Open link</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
)} {canDelete && (
</div> <TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => onDelete(snapshot)}
disabled={isDeleting}
className="h-7 w-7 text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</div> </div>
{/* Message count badge */} {/* Message count badge */}
@ -127,25 +127,25 @@ export function PublicChatSnapshotRow({
{snapshot.public_url} {snapshot.public_url}
</p> </p>
</div> </div>
<TooltipProvider> <TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}> <Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={handleCopyClick} onClick={handleCopyClick}
className="h-6 w-6 shrink-0 text-muted-foreground hover:text-foreground" className="h-6 w-6 shrink-0 text-muted-foreground hover:text-foreground"
> >
{copied ? ( {copied ? (
<Check className="h-3 w-3 text-green-500" /> <Check className="h-3 w-3 text-green-500" />
) : ( ) : (
<Copy className="h-3 w-3" /> <Copy className="h-3 w-3" />
)} )}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{copied ? "Copied!" : "Copy link"}</TooltipContent> <TooltipContent>{copied ? "Copied!" : "Copy link"}</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</div> </div>
{/* Footer: Date + Creator */} {/* Footer: Date + Creator */}
@ -154,33 +154,33 @@ export function PublicChatSnapshotRow({
{member && ( {member && (
<> <>
<span className="text-muted-foreground/30">·</span> <span className="text-muted-foreground/30">·</span>
<TooltipProvider> <TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}> <Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="flex items-center gap-1.5 cursor-default"> <div className="flex items-center gap-1.5 cursor-default">
{member.avatarUrl ? ( {member.avatarUrl ? (
<Image <Image
src={member.avatarUrl} src={member.avatarUrl}
alt={member.name} alt={member.name}
width={18} width={18}
height={18} height={18}
className="h-4.5 w-4.5 rounded-full object-cover shrink-0" className="h-4.5 w-4.5 rounded-full object-cover shrink-0"
/> />
) : ( ) : (
<div className="flex h-4.5 w-4.5 items-center justify-center rounded-full bg-gradient-to-br from-primary/20 to-primary/5 shrink-0"> <div className="flex h-4.5 w-4.5 items-center justify-center rounded-full bg-gradient-to-br from-primary/20 to-primary/5 shrink-0">
<span className="text-[9px] font-semibold text-primary"> <span className="text-[9px] font-semibold text-primary">
{getInitials(member.name)} {getInitials(member.name)}
</span> </span>
</div> </div>
)} )}
<span className="text-[11px] text-muted-foreground/60 truncate max-w-[120px]"> <span className="text-[11px] text-muted-foreground/60 truncate max-w-[120px]">
{member.name} {member.name}
</span> </span>
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom">{member.email || member.name}</TooltipContent> <TooltipContent side="bottom">{member.email || member.name}</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</> </>
)} )}
</div> </div>

View file

@ -272,25 +272,25 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<div className="space-y-4 md:space-y-6"> <div className="space-y-4 md:space-y-6">
{/* Header */} {/* Header */}
<div className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0"> <div className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<Button
variant="secondary"
size="sm"
onClick={() => refreshConfigs()}
disabled={isLoading}
className="gap-2"
>
<RefreshCw className={cn("h-3.5 w-3.5", configsLoading && "animate-spin")} />
Refresh
</Button>
{canCreate && (
<Button <Button
variant="outline" variant="secondary"
onClick={openNewDialog} size="sm"
className="gap-2 bg-white text-black hover:bg-neutral-100 dark:bg-white dark:text-black dark:hover:bg-neutral-200" onClick={() => refreshConfigs()}
disabled={isLoading}
className="gap-2"
> >
Add Image Model <RefreshCw className={cn("h-3.5 w-3.5", configsLoading && "animate-spin")} />
Refresh
</Button> </Button>
)} {canCreate && (
<Button
variant="outline"
onClick={openNewDialog}
className="gap-2 bg-white text-black hover:bg-neutral-100 dark:bg-white dark:text-black dark:hover:bg-neutral-200"
>
Add Image Model
</Button>
)}
</div> </div>
{/* Errors */} {/* Errors */}
@ -420,114 +420,114 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
return ( return (
<div key={config.id}> <div key={config.id}>
<Card className="group relative overflow-hidden transition-all duration-200 border-border/60 hover:shadow-md h-full"> <Card className="group relative overflow-hidden transition-all duration-200 border-border/60 hover:shadow-md h-full">
<CardContent className="p-4 flex flex-col gap-3 h-full"> <CardContent className="p-4 flex flex-col gap-3 h-full">
{/* Header: Name + Actions */} {/* Header: Name + Actions */}
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h4 className="text-sm font-semibold tracking-tight truncate"> <h4 className="text-sm font-semibold tracking-tight truncate">
{config.name} {config.name}
</h4> </h4>
{config.description && ( {config.description && (
<p className="text-[11px] text-muted-foreground/70 truncate mt-0.5"> <p className="text-[11px] text-muted-foreground/70 truncate mt-0.5">
{config.description} {config.description}
</p> </p>
)}
</div>
{(canUpdate || canDelete) && (
<div className="flex items-center gap-0.5 shrink-0 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity duration-150">
{canUpdate && (
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => openEditDialog(config)}
className="h-7 w-7 text-muted-foreground hover:text-foreground"
>
<Edit3 className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>Edit</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{canDelete && (
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => setConfigToDelete(config)}
className="h-7 w-7 text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)} )}
</div> </div>
{(canUpdate || canDelete) && (
{/* Provider + Model */} <div className="flex items-center gap-0.5 shrink-0 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity duration-150">
<div className="flex items-center gap-2 flex-wrap"> {canUpdate && (
{getProviderIcon(config.provider, { className: "size-3.5 shrink-0" })}
<code className="text-[11px] font-mono text-muted-foreground bg-muted/60 px-2 py-0.5 rounded-md truncate max-w-[160px]">
{config.model_name}
</code>
</div>
{/* Footer: Date + Creator */}
<div className="flex items-center gap-2 pt-2 border-t border-border/40 mt-auto">
<span className="text-[11px] text-muted-foreground/60">
{new Date(config.created_at).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
{member && (
<>
<span className="text-muted-foreground/30">·</span>
<TooltipProvider> <TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}> <Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="flex items-center gap-1.5 cursor-default"> <Button
{member.avatarUrl ? ( variant="ghost"
<Image size="icon"
src={member.avatarUrl} onClick={() => openEditDialog(config)}
alt={member.name} className="h-7 w-7 text-muted-foreground hover:text-foreground"
width={18} >
height={18} <Edit3 className="h-3 w-3" />
className="h-4.5 w-4.5 rounded-full object-cover shrink-0" </Button>
/>
) : (
<div className="flex h-4.5 w-4.5 items-center justify-center rounded-full bg-gradient-to-br from-primary/20 to-primary/5 shrink-0">
<span className="text-[9px] font-semibold text-primary">
{getInitials(member.name)}
</span>
</div>
)}
<span className="text-[11px] text-muted-foreground/60 truncate max-w-[120px]">
{member.name}
</span>
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom"> <TooltipContent>Edit</TooltipContent>
{member.email || member.name}
</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</> )}
)} {canDelete && (
</div> <TooltipProvider>
</CardContent> <Tooltip open={isDesktop ? undefined : false}>
</Card> <TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => setConfigToDelete(config)}
className="h-7 w-7 text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)}
</div>
{/* Provider + Model */}
<div className="flex items-center gap-2 flex-wrap">
{getProviderIcon(config.provider, { className: "size-3.5 shrink-0" })}
<code className="text-[11px] font-mono text-muted-foreground bg-muted/60 px-2 py-0.5 rounded-md truncate max-w-[160px]">
{config.model_name}
</code>
</div>
{/* Footer: Date + Creator */}
<div className="flex items-center gap-2 pt-2 border-t border-border/40 mt-auto">
<span className="text-[11px] text-muted-foreground/60">
{new Date(config.created_at).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
{member && (
<>
<span className="text-muted-foreground/30">·</span>
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5 cursor-default">
{member.avatarUrl ? (
<Image
src={member.avatarUrl}
alt={member.name}
width={18}
height={18}
className="h-4.5 w-4.5 rounded-full object-cover shrink-0"
/>
) : (
<div className="flex h-4.5 w-4.5 items-center justify-center rounded-full bg-gradient-to-br from-primary/20 to-primary/5 shrink-0">
<span className="text-[9px] font-semibold text-primary">
{getInitials(member.name)}
</span>
</div>
)}
<span className="text-[11px] text-muted-foreground/60 truncate max-w-[120px]">
{member.name}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{member.email || member.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
</div>
</CardContent>
</Card>
</div> </div>
); );
})} })}
@ -705,20 +705,20 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
</div> </div>
)} )}
{/* Actions */} {/* Actions */}
<div className="flex justify-end gap-3 pt-4 border-t"> <div className="flex justify-end gap-3 pt-4 border-t">
<Button <Button
variant="secondary" variant="secondary"
onClick={() => { onClick={() => {
setIsDialogOpen(false); setIsDialogOpen(false);
setEditingConfig(null); setEditingConfig(null);
resetForm(); resetForm();
}} }}
> >
Cancel Cancel
</Button> </Button>
<Button <Button
onClick={handleFormSubmit} onClick={handleFormSubmit}
disabled={ disabled={
isSubmitting || isSubmitting ||
!formData.name || !formData.name ||

View file

@ -227,15 +227,15 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
<div className="space-y-5 md:space-y-6"> <div className="space-y-5 md:space-y-6">
{/* Header actions */} {/* Header actions */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"
onClick={() => refreshConfigs()} onClick={() => refreshConfigs()}
disabled={isLoading} disabled={isLoading}
className="gap-2" className="gap-2"
> >
<RefreshCw className="h-3.5 w-3.5" /> <RefreshCw className="h-3.5 w-3.5" />
Refresh Refresh
</Button> </Button>
{isAssignmentComplete && !isLoading && !hasError && ( {isAssignmentComplete && !isLoading && !hasError && (
<Badge <Badge

View file

@ -180,24 +180,24 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
<div className="space-y-5 md:space-y-6"> <div className="space-y-5 md:space-y-6">
{/* Header actions */} {/* Header actions */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button
variant="secondary"
size="sm"
onClick={() => refreshConfigs()}
disabled={isLoading}
className="gap-2"
>
<RefreshCw className={cn("h-3.5 w-3.5", isLoading && "animate-spin")} />
Refresh
</Button>
{canCreate && (
<Button <Button
variant="outline" variant="secondary"
onClick={openNewDialog} size="sm"
className="gap-2 bg-white text-black hover:bg-neutral-100 dark:bg-white dark:text-black dark:hover:bg-neutral-200" onClick={() => refreshConfigs()}
disabled={isLoading}
className="gap-2"
> >
Add Configuration <RefreshCw className={cn("h-3.5 w-3.5", isLoading && "animate-spin")} />
Refresh
</Button> </Button>
{canCreate && (
<Button
variant="outline"
onClick={openNewDialog}
className="gap-2 bg-white text-black hover:bg-neutral-100 dark:bg-white dark:text-black dark:hover:bg-neutral-200"
>
Add Configuration
</Button>
)} )}
</div> </div>
@ -330,140 +330,140 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
return ( return (
<div key={config.id}> <div key={config.id}>
<Card className="group relative overflow-hidden transition-all duration-200 border-border/60 hover:shadow-md h-full"> <Card className="group relative overflow-hidden transition-all duration-200 border-border/60 hover:shadow-md h-full">
<CardContent className="p-4 flex flex-col gap-3 h-full"> <CardContent className="p-4 flex flex-col gap-3 h-full">
{/* Header: Name + Actions */} {/* Header: Name + Actions */}
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h4 className="text-sm font-semibold tracking-tight truncate"> <h4 className="text-sm font-semibold tracking-tight truncate">
{config.name} {config.name}
</h4> </h4>
{config.description && ( {config.description && (
<p className="text-[11px] text-muted-foreground/70 truncate mt-0.5"> <p className="text-[11px] text-muted-foreground/70 truncate mt-0.5">
{config.description} {config.description}
</p> </p>
)}
</div>
{(canUpdate || canDelete) && (
<div className="flex items-center gap-0.5 shrink-0 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity duration-150">
{canUpdate && (
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => openEditDialog(config)}
className="h-7 w-7 text-muted-foreground hover:text-foreground"
>
<Edit3 className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>Edit</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{canDelete && (
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => setConfigToDelete(config)}
className="h-7 w-7 text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)} )}
</div> </div>
{(canUpdate || canDelete) && (
{/* Provider + Model */} <div className="flex items-center gap-0.5 shrink-0 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity duration-150">
<div className="flex items-center gap-2 flex-wrap"> {canUpdate && (
{getProviderIcon(config.provider, { className: "size-3.5 shrink-0" })}
<code className="text-[11px] font-mono text-muted-foreground bg-muted/60 px-2 py-0.5 rounded-md truncate max-w-[160px]">
{config.model_name}
</code>
</div>
{/* Feature badges */}
<div className="flex items-center gap-1.5 flex-wrap">
{config.citations_enabled && (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0.5 border-emerald-500/30 text-emerald-700 dark:text-emerald-300 bg-emerald-500/5"
>
<MessageSquareQuote className="h-2.5 w-2.5 mr-1" />
Citations
</Badge>
)}
{!config.use_default_system_instructions &&
config.system_instructions && (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0.5 border-blue-500/30 text-blue-700 dark:text-blue-300 bg-blue-500/5"
>
<FileText className="h-2.5 w-2.5 mr-1" />
Custom
</Badge>
)}
</div>
{/* Footer: Date + Creator */}
<div className="flex items-center gap-2 pt-2 border-t border-border/40 mt-auto">
<span className="text-[11px] text-muted-foreground/60">
{new Date(config.created_at).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
{member && (
<>
<span className="text-muted-foreground/30">·</span>
<TooltipProvider> <TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}> <Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="flex items-center gap-1.5 cursor-default"> <Button
{member.avatarUrl ? ( variant="ghost"
<Image size="icon"
src={member.avatarUrl} onClick={() => openEditDialog(config)}
alt={member.name} className="h-7 w-7 text-muted-foreground hover:text-foreground"
width={18} >
height={18} <Edit3 className="h-3 w-3" />
className="h-4.5 w-4.5 rounded-full object-cover shrink-0" </Button>
/>
) : (
<div className="flex h-4.5 w-4.5 items-center justify-center rounded-full bg-gradient-to-br from-primary/20 to-primary/5 shrink-0">
<span className="text-[9px] font-semibold text-primary">
{getInitials(member.name)}
</span>
</div>
)}
<span className="text-[11px] text-muted-foreground/60 truncate max-w-[120px]">
{member.name}
</span>
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom"> <TooltipContent>Edit</TooltipContent>
{member.email || member.name}
</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</> )}
{canDelete && (
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => setConfigToDelete(config)}
className="h-7 w-7 text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)}
</div>
{/* Provider + Model */}
<div className="flex items-center gap-2 flex-wrap">
{getProviderIcon(config.provider, { className: "size-3.5 shrink-0" })}
<code className="text-[11px] font-mono text-muted-foreground bg-muted/60 px-2 py-0.5 rounded-md truncate max-w-[160px]">
{config.model_name}
</code>
</div>
{/* Feature badges */}
<div className="flex items-center gap-1.5 flex-wrap">
{config.citations_enabled && (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0.5 border-emerald-500/30 text-emerald-700 dark:text-emerald-300 bg-emerald-500/5"
>
<MessageSquareQuote className="h-2.5 w-2.5 mr-1" />
Citations
</Badge>
)}
{!config.use_default_system_instructions &&
config.system_instructions && (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0.5 border-blue-500/30 text-blue-700 dark:text-blue-300 bg-blue-500/5"
>
<FileText className="h-2.5 w-2.5 mr-1" />
Custom
</Badge>
)} )}
</div> </div>
</CardContent>
</Card> {/* Footer: Date + Creator */}
</div> <div className="flex items-center gap-2 pt-2 border-t border-border/40 mt-auto">
); <span className="text-[11px] text-muted-foreground/60">
})} {new Date(config.created_at).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
{member && (
<>
<span className="text-muted-foreground/30">·</span>
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5 cursor-default">
{member.avatarUrl ? (
<Image
src={member.avatarUrl}
alt={member.name}
width={18}
height={18}
className="h-4.5 w-4.5 rounded-full object-cover shrink-0"
/>
) : (
<div className="flex h-4.5 w-4.5 items-center justify-center rounded-full bg-gradient-to-br from-primary/20 to-primary/5 shrink-0">
<span className="text-[9px] font-semibold text-primary">
{getInitials(member.name)}
</span>
</div>
)}
<span className="text-[11px] text-muted-foreground/60 truncate max-w-[120px]">
{member.name}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{member.email || member.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
</div>
</CardContent>
</Card>
</div>
);
})}
</div> </div>
)} )}
</div> </div>

View file

@ -85,16 +85,16 @@ export function MorePagesContent() {
</div> </div>
{isLoading ? ( {isLoading ? (
<Card> <Card>
<CardContent className="flex items-center gap-3 p-3"> <CardContent className="flex items-center gap-3 p-3">
<Skeleton className="h-9 w-9 rounded-full bg-muted" /> <Skeleton className="h-9 w-9 rounded-full bg-muted" />
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<Skeleton className="h-4 w-3/4 bg-muted" /> <Skeleton className="h-4 w-3/4 bg-muted" />
<Skeleton className="h-3 w-1/4 bg-muted" /> <Skeleton className="h-3 w-1/4 bg-muted" />
</div> </div>
<Skeleton className="h-8 w-16 bg-muted" /> <Skeleton className="h-8 w-16 bg-muted" />
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{data?.tasks.map((task) => ( {data?.tasks.map((task) => (
@ -122,14 +122,14 @@ export function MorePagesContent() {
</p> </p>
<p className="text-xs text-muted-foreground">+{task.pages_reward} pages</p> <p className="text-xs text-muted-foreground">+{task.pages_reward} pages</p>
</div> </div>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
disabled={task.completed || completeMutation.isPending} disabled={task.completed || completeMutation.isPending}
onClick={() => handleTaskClick(task)} onClick={() => handleTaskClick(task)}
asChild={!task.completed} asChild={!task.completed}
className="text-muted-foreground hover:text-foreground" className="text-muted-foreground hover:text-foreground"
> >
{task.completed ? ( {task.completed ? (
<span>Done</span> <span>Done</span>
) : ( ) : (
@ -185,11 +185,7 @@ export function MorePagesContent() {
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Button asChild> <Button asChild>
<Link <Link href="https://cal.com/mod-rohan" target="_blank" rel="noopener noreferrer">
href="https://cal.com/mod-rohan"
target="_blank"
rel="noopener noreferrer"
>
<IconCalendar className="h-4 w-4" /> <IconCalendar className="h-4 w-4" />
Schedule a Meeting Schedule a Meeting
</Link> </Link>

View file

@ -176,17 +176,17 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
</CardContent> </CardContent>
</Card> </Card>
{/* Action Buttons */} {/* Action Buttons */}
<div className="flex justify-end pt-3 md:pt-4"> <div className="flex justify-end pt-3 md:pt-4">
<Button <Button
variant="outline" variant="outline"
onClick={handleSave} onClick={handleSave}
disabled={!hasChanges || saving} disabled={!hasChanges || saving}
className="gap-2 bg-white text-black hover:bg-neutral-100 dark:bg-white dark:text-black dark:hover:bg-neutral-200" className="gap-2 bg-white text-black hover:bg-neutral-100 dark:bg-white dark:text-black dark:hover:bg-neutral-200"
> >
{saving ? "Saving" : "Save Instructions"} {saving ? "Saving" : "Save Instructions"}
</Button> </Button>
</div> </div>
</div> </div>
); );
} }

View file

@ -682,11 +682,11 @@ function PermissionsEditor({
return ( return (
<div key={category} className="rounded-lg border border-border/60 overflow-hidden"> <div key={category} className="rounded-lg border border-border/60 overflow-hidden">
<button <button
type="button" type="button"
className="w-full flex items-center justify-between px-3 py-2.5 cursor-pointer hover:bg-muted/40 transition-colors" className="w-full flex items-center justify-between px-3 py-2.5 cursor-pointer hover:bg-muted/40 transition-colors"
onClick={() => toggleCategoryExpanded(category)} onClick={() => toggleCategoryExpanded(category)}
> >
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<IconComponent className="h-4 w-4 text-muted-foreground shrink-0" /> <IconComponent className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="font-medium text-sm">{config.label}</span> <span className="font-medium text-sm">{config.label}</span>
@ -702,10 +702,7 @@ function PermissionsEditor({
aria-label={`Select all ${config.label} permissions`} aria-label={`Select all ${config.label} permissions`}
/> />
<div <div
className={cn( className={cn("transition-transform duration-200", isExpanded && "rotate-180")}
"transition-transform duration-200",
isExpanded && "rotate-180"
)}
> >
<svg <svg
className="h-4 w-4 text-muted-foreground" className="h-4 w-4 text-muted-foreground"
@ -933,11 +930,11 @@ function CreateRoleDialog({
/> />
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-3 px-5 py-3 shrink-0"> <div className="flex items-center justify-end gap-3 px-5 py-3 shrink-0">
<Button variant="secondary" onClick={handleClose}> <Button variant="secondary" onClick={handleClose}>
Cancel Cancel
</Button> </Button>
<Button onClick={handleCreate} disabled={creating || !name.trim()}> <Button onClick={handleCreate} disabled={creating || !name.trim()}>
{creating ? ( {creating ? (
<> <>
<Spinner size="sm" className="mr-2" /> <Spinner size="sm" className="mr-2" />
@ -1091,10 +1088,10 @@ function EditRoleDialog({
/> />
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-3 px-5 py-3 border-t shrink-0"> <div className="flex items-center justify-end gap-3 px-5 py-3 border-t shrink-0">
<Button variant="secondary" onClick={() => onOpenChange(false)}> <Button variant="secondary" onClick={() => onOpenChange(false)}>
Cancel Cancel
</Button> </Button>
<Button onClick={handleSave} disabled={saving || !name.trim()}> <Button onClick={handleSave} disabled={saving || !name.trim()}>
{saving ? ( {saving ? (
<> <>

View file

@ -60,11 +60,11 @@ export function SettingsDialog({
type="button" type="button"
onClick={() => onItemChange(item.value)} onClick={() => onItemChange(item.value)}
className={cn( className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors text-left focus:outline-none focus-visible:outline-none", "flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors text-left focus:outline-none focus-visible:outline-none",
activeItem === item.value activeItem === item.value
? "bg-accent text-accent-foreground" ? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground" : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
)} )}
> >
{item.icon} {item.icon}
{item.label} {item.label}
@ -94,11 +94,11 @@ export function SettingsDialog({
type="button" type="button"
onClick={() => handleItemChange(item.value)} onClick={() => handleItemChange(item.value)}
className={cn( className={cn(
"flex items-center gap-2 whitespace-nowrap rounded-full px-3 py-1.5 text-xs font-medium transition-colors shrink-0 focus:outline-none focus-visible:outline-none", "flex items-center gap-2 whitespace-nowrap rounded-full px-3 py-1.5 text-xs font-medium transition-colors shrink-0 focus:outline-none focus-visible:outline-none",
activeItem === item.value activeItem === item.value
? "bg-accent text-accent-foreground" ? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground" : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
)} )}
> >
{item.icon} {item.icon}
{item.label} {item.label}

View file

@ -562,17 +562,17 @@ export function LLMConfigForm({
compact ? "justify-end" : "justify-center sm:justify-end" compact ? "justify-end" : "justify-center sm:justify-end"
)} )}
> >
{onCancel && ( {onCancel && (
<Button <Button
type="button" type="button"
variant="secondary" variant="secondary"
onClick={onCancel} onClick={onCancel}
disabled={isSubmitting} disabled={isSubmitting}
className="text-xs sm:text-sm h-9 sm:h-10" className="text-xs sm:text-sm h-9 sm:h-10"
> >
Cancel Cancel
</Button> </Button>
)} )}
<Button <Button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}