mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-16 23:01:06 +02:00
chore: ran linting
This commit is contained in:
parent
bc1f31b481
commit
ac0f2fa2eb
33 changed files with 688 additions and 661 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -428,4 +428,4 @@ class ChucksHybridSearchRetriever:
|
||||||
search_space_id,
|
search_space_id,
|
||||||
document_type,
|
document_type,
|
||||||
)
|
)
|
||||||
return final_docs
|
return final_docs
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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")}>
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 ||
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue