Merge branch 'main' into embedding-tooltip
This commit is contained in:
commit
7232d8ea23
8 changed files with 320 additions and 219 deletions
44
.env.example
44
.env.example
|
|
@ -1,42 +1,60 @@
|
||||||
# Ingestion Configuration
|
# Ingestion Configuration
|
||||||
# Set to true to disable Langflow ingestion and use traditional OpenRAG processor
|
# Set to true to disable Langflow ingestion and use the traditional OpenRAG processor.
|
||||||
# If unset or false, Langflow pipeline will be used (default: upload -> ingest -> delete)
|
# If unset or false, the Langflow pipeline is used (default: upload -> ingest -> delete).
|
||||||
DISABLE_INGEST_WITH_LANGFLOW=false
|
DISABLE_INGEST_WITH_LANGFLOW=false
|
||||||
# make one like so https://docs.langflow.org/api-keys-and-authentication#langflow-secret-key
|
|
||||||
|
# Create a Langflow secret key:
|
||||||
|
# https://docs.langflow.org/api-keys-and-authentication#langflow-secret-key
|
||||||
LANGFLOW_SECRET_KEY=
|
LANGFLOW_SECRET_KEY=
|
||||||
|
|
||||||
# flow ids for chat and ingestion flows
|
|
||||||
|
# Flow IDs for chat and ingestion
|
||||||
LANGFLOW_CHAT_FLOW_ID=1098eea1-6649-4e1d-aed1-b77249fb8dd0
|
LANGFLOW_CHAT_FLOW_ID=1098eea1-6649-4e1d-aed1-b77249fb8dd0
|
||||||
LANGFLOW_INGEST_FLOW_ID=5488df7c-b93f-4f87-a446-b67028bc0813
|
LANGFLOW_INGEST_FLOW_ID=5488df7c-b93f-4f87-a446-b67028bc0813
|
||||||
# Ingest flow using docling
|
# Ingest flow using Docling
|
||||||
# LANGFLOW_INGEST_FLOW_ID=1402618b-e6d1-4ff2-9a11-d6ce71186915
|
# LANGFLOW_INGEST_FLOW_ID=1402618b-e6d1-4ff2-9a11-d6ce71186915
|
||||||
NUDGES_FLOW_ID=ebc01d31-1976-46ce-a385-b0240327226c
|
NUDGES_FLOW_ID=ebc01d31-1976-46ce-a385-b0240327226c
|
||||||
|
|
||||||
# Set a strong admin password for OpenSearch; a bcrypt hash is generated at
|
|
||||||
# container startup from this value. Do not commit real secrets.
|
# OpenSearch Auth
|
||||||
# must match the hashed password in secureconfig, must change for secure deployment!!!
|
# Set a strong admin password for OpenSearch.
|
||||||
|
# A bcrypt hash is generated at container startup from this value.
|
||||||
|
# Do not commit real secrets.
|
||||||
|
# Must be changed for secure deployments.
|
||||||
OPENSEARCH_PASSWORD=
|
OPENSEARCH_PASSWORD=
|
||||||
|
|
||||||
# make here https://console.cloud.google.com/apis/credentials
|
|
||||||
|
# Google OAuth
|
||||||
|
# Create credentials here:
|
||||||
|
# https://console.cloud.google.com/apis/credentials
|
||||||
GOOGLE_OAUTH_CLIENT_ID=
|
GOOGLE_OAUTH_CLIENT_ID=
|
||||||
GOOGLE_OAUTH_CLIENT_SECRET=
|
GOOGLE_OAUTH_CLIENT_SECRET=
|
||||||
|
|
||||||
# Azure app registration credentials for SharePoint/OneDrive
|
|
||||||
|
# Microsoft (SharePoint/OneDrive) OAuth
|
||||||
|
# Azure app registration credentials.
|
||||||
MICROSOFT_GRAPH_OAUTH_CLIENT_ID=
|
MICROSOFT_GRAPH_OAUTH_CLIENT_ID=
|
||||||
MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET=
|
MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET=
|
||||||
|
|
||||||
# OPTIONAL: dns routable from google (etc.) to handle continous ingest (something like ngrok works). This enables continous ingestion
|
|
||||||
|
# Webhooks (optional)
|
||||||
|
# Public, DNS-resolvable base URL (e.g., via ngrok) for continuous ingestion.
|
||||||
WEBHOOK_BASE_URL=
|
WEBHOOK_BASE_URL=
|
||||||
|
|
||||||
|
|
||||||
|
# API Keys
|
||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=
|
AWS_ACCESS_KEY_ID=
|
||||||
AWS_SECRET_ACCESS_KEY=
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
|
||||||
# OPTIONAL url for openrag link to langflow in the UI
|
|
||||||
|
# Langflow UI URL (optional)
|
||||||
|
# Public URL to link OpenRAG to Langflow in the UI.
|
||||||
LANGFLOW_PUBLIC_URL=
|
LANGFLOW_PUBLIC_URL=
|
||||||
|
|
||||||
# Langflow auth
|
|
||||||
|
# Langflow Auth
|
||||||
LANGFLOW_AUTO_LOGIN=False
|
LANGFLOW_AUTO_LOGIN=False
|
||||||
LANGFLOW_SUPERUSER=
|
LANGFLOW_SUPERUSER=
|
||||||
LANGFLOW_SUPERUSER_PASSWORD=
|
LANGFLOW_SUPERUSER_PASSWORD=
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Plus } from "lucide-react";
|
||||||
import { Loader2, Plus } from "lucide-react";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
useGetFiltersSearchQuery,
|
useGetFiltersSearchQuery,
|
||||||
|
|
@ -65,97 +64,102 @@ export function KnowledgeFilterList({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
<div className="flex flex-col gap-2 px-3 !mb-12 mt-0 h-full overflow-y-auto">
|
<div className="px-3 flex-1 min-h-0 flex flex-col">
|
||||||
<div className="flex items-center w-full justify-between pl-3">
|
<div className="flex-shrink-0">
|
||||||
<div className="text-sm font-medium text-muted-foreground">
|
<div className="flex items-center justify-between mb-3 ml-3 mr-2">
|
||||||
Knowledge Filters
|
<h3 className="text-xs font-medium text-muted-foreground">
|
||||||
</div>
|
Knowledge Filters
|
||||||
<Button
|
</h3>
|
||||||
variant="ghost"
|
<button
|
||||||
size="sm"
|
type="button"
|
||||||
onClick={handleCreateNew}
|
className="p-1 hover:bg-accent rounded"
|
||||||
title="Create New Filter"
|
onClick={handleCreateNew}
|
||||||
className="!h-8 w-8 px-0 text-muted-foreground"
|
title="Create New Filter"
|
||||||
>
|
|
||||||
<Plus className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center justify-center p-4">
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
<span className="ml-2 text-sm text-muted-foreground">
|
|
||||||
Loading...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : filters.length === 0 ? (
|
|
||||||
<div className="py-2 px-4 text-sm text-muted-foreground">
|
|
||||||
{searchQuery ? "No filters found" : "No saved filters"}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
filters.map((filter) => (
|
|
||||||
<div
|
|
||||||
key={filter.id}
|
|
||||||
onClick={() => handleFilterSelect(filter)}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-3 px-3 py-2 w-full rounded-lg hover:bg-accent hover:text-accent-foreground cursor-pointer group transition-colors",
|
|
||||||
selectedFilter?.id === filter.id &&
|
|
||||||
"active bg-accent text-accent-foreground"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1 flex-1 min-w-0">
|
<Plus className="h-4 w-4 text-muted-foreground" />
|
||||||
<div className="flex items-center gap-2">
|
</button>
|
||||||
{(() => {
|
</div>
|
||||||
const parsed = parseQueryData(
|
<div className="overflow-y-auto scrollbar-hide space-y-1">
|
||||||
filter.query_data
|
{loading ? (
|
||||||
) as ParsedQueryData;
|
<div className="text-[13px] text-muted-foreground p-2 ml-1">
|
||||||
const Icon = iconKeyToComponent(parsed.icon);
|
Loading...
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-center w-5 h-5 rounded flex-shrink-0 transition-colors",
|
|
||||||
filterAccentClasses[parsed.color],
|
|
||||||
parsed.color === "zinc" &&
|
|
||||||
"group-hover:bg-background group-[.active]:bg-background"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{Icon && <Icon className="h-3 w-3" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
<div className="text-sm font-medium truncate group-hover:text-accent-foreground">
|
|
||||||
{filter.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{filter.description && (
|
|
||||||
<div className="text-xs text-muted-foreground line-clamp-2">
|
|
||||||
{filter.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{new Date(filter.created_at).toLocaleDateString(undefined, {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<span className="text-xs bg-muted text-muted-foreground px-1 py-0.5 rounded-sm group-hover:bg-background group-[.active]:bg-background transition-colors">
|
|
||||||
{(() => {
|
|
||||||
const dataSources = parseQueryData(filter.query_data)
|
|
||||||
.filters.data_sources;
|
|
||||||
if (dataSources[0] === "*") return "All sources";
|
|
||||||
const count = dataSources.length;
|
|
||||||
return `${count} ${count === 1 ? "source" : "sources"}`;
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : filters.length === 0 ? (
|
||||||
))
|
<div className="text-[13px] text-muted-foreground p-2 ml-1">
|
||||||
)}
|
{searchQuery ? "No filters found" : "No saved filters"}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filters.map(filter => (
|
||||||
|
<div
|
||||||
|
key={filter.id}
|
||||||
|
onClick={() => handleFilterSelect(filter)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 px-3 py-2 w-full rounded-lg hover:bg-accent hover:text-accent-foreground cursor-pointer group transition-colors",
|
||||||
|
selectedFilter?.id === filter.id &&
|
||||||
|
"active bg-accent text-accent-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1 flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{(() => {
|
||||||
|
const parsed = parseQueryData(
|
||||||
|
filter.query_data
|
||||||
|
) as ParsedQueryData;
|
||||||
|
const Icon = iconKeyToComponent(parsed.icon);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center w-5 h-5 rounded transition-colors",
|
||||||
|
filterAccentClasses[parsed.color],
|
||||||
|
parsed.color === "zinc" &&
|
||||||
|
"group-hover:bg-background group-[.active]:bg-background"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Icon && <Icon className="h-3 w-3" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
<div className="text-sm font-medium truncate group-hover:text-accent-foreground">
|
||||||
|
{filter.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{filter.description && (
|
||||||
|
<div className="text-xs text-muted-foreground line-clamp-2">
|
||||||
|
{filter.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{new Date(filter.created_at).toLocaleDateString(
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs bg-muted text-muted-foreground px-1 py-0.5 rounded-sm group-hover:bg-background group-[.active]:bg-background transition-colors">
|
||||||
|
{(() => {
|
||||||
|
const dataSources = parseQueryData(filter.query_data)
|
||||||
|
.filters.data_sources;
|
||||||
|
if (dataSources[0] === "*") return "All sources";
|
||||||
|
const count = dataSources.length;
|
||||||
|
return `${count} ${
|
||||||
|
count === 1 ? "source" : "sources"
|
||||||
|
}`;
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Create flow moved to panel create mode */}
|
||||||
</div>
|
</div>
|
||||||
{/* Create flow moved to panel create mode */}
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useGetConversationsQuery, type ChatConversation } from "@/app/api/queries/useGetConversationsQuery";
|
import {
|
||||||
|
useGetConversationsQuery,
|
||||||
|
type ChatConversation,
|
||||||
|
} from "@/app/api/queries/useGetConversationsQuery";
|
||||||
import { KnowledgeFilterDropdown } from "@/components/knowledge-filter-dropdown";
|
import { KnowledgeFilterDropdown } from "@/components/knowledge-filter-dropdown";
|
||||||
import { ModeToggle } from "@/components/mode-toggle";
|
import { ModeToggle } from "@/components/mode-toggle";
|
||||||
import { Navigation } from "@/components/navigation";
|
import { Navigation } from "@/components/navigation";
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
FileText,
|
FileText,
|
||||||
Library,
|
Library,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
MoreHorizontal,
|
|
||||||
Plus,
|
Plus,
|
||||||
Settings2,
|
Settings2,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
|
@ -111,7 +110,7 @@ export function Navigation({
|
||||||
) {
|
) {
|
||||||
// Filter out the deleted conversation and find the next one
|
// Filter out the deleted conversation and find the next one
|
||||||
const remainingConversations = conversations.filter(
|
const remainingConversations = conversations.filter(
|
||||||
(conv) => conv.response_id !== conversationToDelete.response_id,
|
conv => conv.response_id !== conversationToDelete.response_id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (remainingConversations.length > 0) {
|
if (remainingConversations.length > 0) {
|
||||||
|
|
@ -132,7 +131,7 @@ export function Navigation({
|
||||||
setDeleteModalOpen(false);
|
setDeleteModalOpen(false);
|
||||||
setConversationToDelete(null);
|
setConversationToDelete(null);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: error => {
|
||||||
toast.error(`Failed to delete conversation: ${error.message}`);
|
toast.error(`Failed to delete conversation: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -164,7 +163,7 @@ export function Navigation({
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("fileUploadStart", {
|
new CustomEvent("fileUploadStart", {
|
||||||
detail: { filename: file.name },
|
detail: { filename: file.name },
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -188,7 +187,7 @@ export function Navigation({
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
error: "Failed to process document",
|
error: "Failed to process document",
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trigger loading end event
|
// Trigger loading end event
|
||||||
|
|
@ -208,7 +207,7 @@ export function Navigation({
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("fileUploaded", {
|
new CustomEvent("fileUploaded", {
|
||||||
detail: { file, result },
|
detail: { file, result },
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trigger loading end event
|
// Trigger loading end event
|
||||||
|
|
@ -222,7 +221,7 @@ export function Navigation({
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("fileUploadError", {
|
new CustomEvent("fileUploadError", {
|
||||||
detail: { filename: file.name, error: "Failed to process document" },
|
detail: { filename: file.name, error: "Failed to process document" },
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -244,7 +243,7 @@ export function Navigation({
|
||||||
|
|
||||||
const handleDeleteConversation = (
|
const handleDeleteConversation = (
|
||||||
conversation: ChatConversation,
|
conversation: ChatConversation,
|
||||||
event?: React.MouseEvent,
|
event?: React.MouseEvent
|
||||||
) => {
|
) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -256,7 +255,7 @@ export function Navigation({
|
||||||
|
|
||||||
const handleContextMenuAction = (
|
const handleContextMenuAction = (
|
||||||
action: string,
|
action: string,
|
||||||
conversation: ChatConversation,
|
conversation: ChatConversation
|
||||||
) => {
|
) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "delete":
|
case "delete":
|
||||||
|
|
@ -332,33 +331,33 @@ export function Navigation({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-background">
|
<div className="flex flex-col h-full bg-background">
|
||||||
<div className="px-3 py-2 flex-shrink-0">
|
<div className="px-4 py-2 flex-shrink-0">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{routes.map((route) => (
|
{routes.map(route => (
|
||||||
<div key={route.href}>
|
<div key={route.href}>
|
||||||
<Link
|
<Link
|
||||||
href={route.href}
|
href={route.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm group flex p-3 w-full justify-start font-medium cursor-pointer hover:bg-accent hover:text-accent-foreground rounded-lg transition-all",
|
"text-[13px] group flex p-3 w-full justify-start font-medium cursor-pointer hover:bg-accent hover:text-accent-foreground rounded-lg transition-all",
|
||||||
route.active
|
route.active
|
||||||
? "bg-accent text-accent-foreground shadow-sm"
|
? "bg-accent text-accent-foreground shadow-sm"
|
||||||
: "text-foreground hover:text-accent-foreground",
|
: "text-foreground hover:text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center flex-1">
|
<div className="flex items-center flex-1">
|
||||||
<route.icon
|
<route.icon
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-4 w-4 mr-3 shrink-0",
|
"h-[18px] w-[18px] mr-2 shrink-0",
|
||||||
route.active
|
route.active
|
||||||
? "text-accent-foreground"
|
? "text-muted-foreground"
|
||||||
: "text-muted-foreground group-hover:text-foreground",
|
: "text-muted-foreground group-hover:text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{route.label}
|
{route.label}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
{route.label === "Settings" && (
|
{route.label === "Settings" && (
|
||||||
<div className="mx-3 my-2 border-t border-border/40" />
|
<div className="my-2 border-t border-border" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -374,11 +373,11 @@ export function Navigation({
|
||||||
|
|
||||||
{/* Chat Page Specific Sections */}
|
{/* Chat Page Specific Sections */}
|
||||||
{isOnChatPage && (
|
{isOnChatPage && (
|
||||||
<div className="flex-1 min-h-0 flex flex-col">
|
<div className="flex-1 min-h-0 flex flex-col px-4">
|
||||||
{/* Conversations Section */}
|
{/* Conversations Section */}
|
||||||
<div className="px-3 flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3 mx-3">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground">
|
<h3 className="text-xs font-medium text-muted-foreground">
|
||||||
Conversations
|
Conversations
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
|
|
@ -393,11 +392,11 @@ export function Navigation({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-3 flex-1 min-h-0 flex flex-col">
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
{/* Conversations List - grows naturally, doesn't fill all space */}
|
{/* Conversations List - grows naturally, doesn't fill all space */}
|
||||||
<div className="flex-shrink-0 overflow-y-auto scrollbar-hide space-y-1 max-h-full">
|
<div className="flex-shrink-0 overflow-y-auto scrollbar-hide space-y-1 max-h-full">
|
||||||
{loadingNewConversation || isConversationsLoading ? (
|
{loadingNewConversation || isConversationsLoading ? (
|
||||||
<div className="text-sm text-muted-foreground p-2">
|
<div className="text-[13px] text-muted-foreground p-2">
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -406,7 +405,7 @@ export function Navigation({
|
||||||
{placeholderConversation && (
|
{placeholderConversation && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full p-2 rounded-lg bg-accent/50 border border-dashed border-accent cursor-pointer group text-left"
|
className="w-full px-3 rounded-lg bg-accent border border-dashed border-accent cursor-pointer group text-left h-[44px]"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Don't load placeholder as a real conversation, just focus the input
|
// Don't load placeholder as a real conversation, just focus the input
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
|
|
@ -414,7 +413,7 @@ export function Navigation({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-sm font-medium text-foreground mb-1 truncate">
|
<div className="text-[13px] font-medium text-foreground truncate">
|
||||||
{placeholderConversation.title}
|
{placeholderConversation.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
|
|
@ -425,15 +424,15 @@ export function Navigation({
|
||||||
|
|
||||||
{/* Show regular conversations */}
|
{/* Show regular conversations */}
|
||||||
{conversations.length === 0 && !placeholderConversation ? (
|
{conversations.length === 0 && !placeholderConversation ? (
|
||||||
<div className="text-sm text-muted-foreground p-2">
|
<div className="text-[13px] text-muted-foreground py-2 pl-3">
|
||||||
No conversations yet
|
No conversations yet
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
conversations.map((conversation) => (
|
conversations.map(conversation => (
|
||||||
<button
|
<button
|
||||||
key={conversation.response_id}
|
key={conversation.response_id}
|
||||||
type="button"
|
type="button"
|
||||||
className={`w-full px-3 pr-2 h-11 rounded-lg group relative text-left ${
|
className={`w-full px-3 h-11 rounded-lg group relative text-left ${
|
||||||
loading
|
loading
|
||||||
? "opacity-50 cursor-not-allowed"
|
? "opacity-50 cursor-not-allowed"
|
||||||
: "hover:bg-accent cursor-pointer"
|
: "hover:bg-accent cursor-pointer"
|
||||||
|
|
@ -456,17 +455,22 @@ export function Navigation({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger disabled={loading || deleteSessionMutation.isPending} asChild>
|
<DropdownMenuTrigger
|
||||||
|
disabled={
|
||||||
|
loading || deleteSessionMutation.isPending
|
||||||
|
}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100 data-[state=open]:text-foreground transition-opacity p-1 hover:bg-accent rounded text-muted-foreground hover:text-foreground ml-2 flex-shrink-0 cursor-pointer"
|
className="opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100 data-[state=open]:text-foreground transition-opacity p-1 hover:bg-accent rounded text-muted-foreground hover:text-foreground ml-2 flex-shrink-0 cursor-pointer"
|
||||||
title="More options"
|
title="More options"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={e => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
@ -479,14 +483,14 @@ export function Navigation({
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="end"
|
align="end"
|
||||||
className="w-48"
|
className="w-48"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleContextMenuAction(
|
handleContextMenuAction(
|
||||||
"delete",
|
"delete",
|
||||||
conversation,
|
conversation
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer text-destructive focus:text-destructive"
|
className="cursor-pointer text-destructive focus:text-destructive"
|
||||||
|
|
@ -506,8 +510,8 @@ export function Navigation({
|
||||||
|
|
||||||
{/* Conversation Knowledge Section - appears right after last conversation */}
|
{/* Conversation Knowledge Section - appears right after last conversation */}
|
||||||
<div className="flex-shrink-0 mt-4">
|
<div className="flex-shrink-0 mt-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3 mx-3">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground">
|
<h3 className="text-xs font-medium text-muted-foreground">
|
||||||
Conversation knowledge
|
Conversation knowledge
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
|
|
@ -526,16 +530,16 @@ export function Navigation({
|
||||||
className="hidden"
|
className="hidden"
|
||||||
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
|
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
|
||||||
/>
|
/>
|
||||||
<div className="overflow-y-auto scrollbar-hide space-y-1 max-h-40">
|
<div className="overflow-y-auto scrollbar-hide space-y-1">
|
||||||
{conversationDocs.length === 0 ? (
|
{conversationDocs.length === 0 ? (
|
||||||
<div className="text-sm text-muted-foreground p-2">
|
<div className="text-[13px] text-muted-foreground py-2 px-3">
|
||||||
No documents yet
|
No documents yet
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
conversationDocs.map((doc) => (
|
conversationDocs.map(doc => (
|
||||||
<div
|
<div
|
||||||
key={`${doc.filename}-${doc.uploadTime.getTime()}`}
|
key={`${doc.filename}-${doc.uploadTime.getTime()}`}
|
||||||
className="p-2 rounded-lg hover:bg-accent cursor-pointer group flex items-center"
|
className="w-full px-3 h-11 rounded-lg hover:bg-accent cursor-pointer group flex items-center"
|
||||||
>
|
>
|
||||||
<FileText className="h-4 w-4 mr-2 text-muted-foreground flex-shrink-0" />
|
<FileText className="h-4 w-4 mr-2 text-muted-foreground flex-shrink-0" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
// Calculate active tasks for the bell icon
|
// Calculate active tasks for the bell icon
|
||||||
const activeTasks = tasks.filter(
|
const activeTasks = tasks.filter(
|
||||||
(task) =>
|
task =>
|
||||||
task.status === "pending" ||
|
task.status === "pending" ||
|
||||||
task.status === "running" ||
|
task.status === "running" ||
|
||||||
task.status === "processing"
|
task.status === "processing"
|
||||||
|
|
@ -84,16 +84,16 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||||
// For all other pages, render with Langflow-styled navigation and task menu
|
// For all other pages, render with Langflow-styled navigation and task menu
|
||||||
return (
|
return (
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
<header className="header-arrangement bg-background sticky top-0 z-50">
|
<header className="header-arrangement bg-background sticky top-0 z-50 h-10">
|
||||||
<div className="header-start-display px-4">
|
<div className="header-start-display px-[16px]">
|
||||||
{/* Logo/Title */}
|
{/* Logo/Title */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center">
|
||||||
<Logo className="fill-primary" width={24} height={22} />
|
<Logo className="fill-primary" width={24} height={22} />
|
||||||
<span className="text-lg font-semibold">OpenRAG</span>
|
<span className="text-lg font-semibold pl-2.5">OpenRAG</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="header-end-division">
|
<div className="header-end-division">
|
||||||
<div className="header-end-display">
|
<div className="justify-end flex items-center">
|
||||||
{/* Knowledge Filter Dropdown */}
|
{/* Knowledge Filter Dropdown */}
|
||||||
{/* <KnowledgeFilterDropdown
|
{/* <KnowledgeFilterDropdown
|
||||||
selectedFilter={selectedFilter}
|
selectedFilter={selectedFilter}
|
||||||
|
|
@ -107,26 +107,24 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||||
{/* <DiscordLink inviteCode="EqksyE2EX9" /> */}
|
{/* <DiscordLink inviteCode="EqksyE2EX9" /> */}
|
||||||
|
|
||||||
{/* Task Notification Bell */}
|
{/* Task Notification Bell */}
|
||||||
<Button
|
<button
|
||||||
variant="ghost"
|
|
||||||
size="iconSm"
|
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
className="relative"
|
className="h-8 w-8 hover:bg-muted rounded-lg flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<Bell className="h-4 w-4 text-muted-foreground" />
|
<Bell size={16} className="text-muted-foreground" />
|
||||||
{activeTasks.length > 0 && (
|
{activeTasks.length > 0 && (
|
||||||
<div className="header-notifications" />
|
<div className="header-notifications" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</button>
|
||||||
|
|
||||||
{/* Separator */}
|
{/* Separator */}
|
||||||
<div className="w-px h-6 bg-border" />
|
<div className="w-px h-6 bg-border mx-3" />
|
||||||
|
|
||||||
<UserNav />
|
<UserNav />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="side-bar-arrangement bg-background fixed left-0 top-[53px] bottom-0 md:flex hidden">
|
<div className="side-bar-arrangement bg-background fixed left-0 top-[40px] bottom-0 md:flex hidden pt-1">
|
||||||
<Navigation
|
<Navigation
|
||||||
conversations={conversations}
|
conversations={conversations}
|
||||||
isConversationsLoading={isConversationsLoading}
|
isConversationsLoading={isConversationsLoading}
|
||||||
|
|
|
||||||
68
frontend/src/components/ui/buttonTheme.tsx
Normal file
68
frontend/src/components/ui/buttonTheme.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Monitor, Moon, Sun } from "lucide-react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
|
export const ThemeButtons = () => {
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
const [selectedTheme, setSelectedTheme] = useState("dark");
|
||||||
|
|
||||||
|
// Sync local state with theme context
|
||||||
|
useEffect(() => {
|
||||||
|
if (theme) {
|
||||||
|
setSelectedTheme(theme);
|
||||||
|
}
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const handleThemeChange = (newTheme: string) => {
|
||||||
|
setSelectedTheme(newTheme);
|
||||||
|
setTheme(newTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center border border-border rounded-full">
|
||||||
|
{/* Light Theme Button */}
|
||||||
|
<button
|
||||||
|
className={`h-6 w-6 rounded-full flex items-center justify-center ${
|
||||||
|
selectedTheme === "light"
|
||||||
|
? "bg-amber-400 text-primary"
|
||||||
|
: "text-foreground hover:bg-amber-400 hover:text-background"
|
||||||
|
}`}
|
||||||
|
onClick={() => handleThemeChange("light")}
|
||||||
|
data-testid="menu_light_button"
|
||||||
|
id="menu_light_button"
|
||||||
|
>
|
||||||
|
<Sun className="h-4 w-4 rounded-full" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dark Theme Button */}
|
||||||
|
<button
|
||||||
|
className={`h-6 w-6 rounded-full flex items-center justify-center ${
|
||||||
|
selectedTheme === "dark"
|
||||||
|
? "bg-purple-500/20 text-purple-500 hover:bg-purple-500/20 hover:text-purple-500"
|
||||||
|
: "text-foreground hover:bg-purple-500/20 hover:text-purple-500"
|
||||||
|
}`}
|
||||||
|
onClick={() => handleThemeChange("dark")}
|
||||||
|
data-testid="menu_dark_button"
|
||||||
|
id="menu_dark_button"
|
||||||
|
>
|
||||||
|
<Moon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* System Theme Button */}
|
||||||
|
<button
|
||||||
|
className={`h-6 w-6 rounded-full flex items-center justify-center ${
|
||||||
|
selectedTheme === "system"
|
||||||
|
? "bg-foreground text-background"
|
||||||
|
: "hover:bg-foreground hover:text-background"
|
||||||
|
}`}
|
||||||
|
onClick={() => handleThemeChange("system")}
|
||||||
|
data-testid="menu_system_button"
|
||||||
|
id="menu_system_button"
|
||||||
|
>
|
||||||
|
<Monitor className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeButtons;
|
||||||
|
|
@ -1,94 +1,100 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useAuth } from "@/contexts/auth-context"
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
import { LogIn, LogOut, User, Moon, Sun, ChevronsUpDown } from "lucide-react"
|
import { LogOut, User, Moon, Sun, ChevronsUpDown } from "lucide-react";
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes";
|
||||||
|
import ThemeButtons from "./ui/buttonTheme";
|
||||||
|
|
||||||
export function UserNav() {
|
export function UserNav() {
|
||||||
const { user, isLoading, isAuthenticated, isNoAuthMode, login, logout } = useAuth()
|
const { user, isLoading, isAuthenticated, isNoAuthMode, login, logout } =
|
||||||
const { theme, setTheme } = useTheme()
|
useAuth();
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return <div className="h-8 w-8 rounded-full bg-muted animate-pulse" />;
|
||||||
<div className="h-8 w-8 rounded-full bg-muted animate-pulse" />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// In no-auth mode, show a simple theme switcher instead of auth UI
|
// In no-auth mode, show a simple theme switcher instead of auth UI
|
||||||
if (isNoAuthMode) {
|
if (isNoAuthMode) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<button
|
||||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||||
variant="outline"
|
className="flex justify-center items-center gap-2 h-8 w-8 mr-2 rounded-md hover:bg-muted rounded-lg "
|
||||||
size="sm"
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
>
|
||||||
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
{theme === "dark" ? (
|
||||||
</Button>
|
<Sun size={16} className="text-muted-foreground" />
|
||||||
)
|
) : (
|
||||||
|
<Moon size={16} className="text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<button
|
||||||
onClick={login}
|
onClick={login}
|
||||||
variant="outline"
|
className="flex items-center gap-2 h-7 px-3 mr-2 rounded-md bg-primary hover:bg-primary/90 text-primary-foreground text-[13px] line-height-[16px]"
|
||||||
size="sm"
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
>
|
||||||
<LogIn className="h-4 w-4" />
|
|
||||||
Sign In
|
Sign In
|
||||||
</Button>
|
</button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="flex items-center gap-1 h-8 px-1 rounded-full">
|
<button className="hover:bg-accent rounded-lg pl-[4px] p-[3px] flex items-center justify-center">
|
||||||
<Avatar className="h-6 w-6">
|
<Avatar className="rounded-md w-7 h-7">
|
||||||
<AvatarImage src={user?.picture} alt={user?.name} />
|
<AvatarImage
|
||||||
<AvatarFallback className="text-xs">
|
width={16}
|
||||||
{user?.name ? user.name.charAt(0).toUpperCase() : <User className="h-3 w-3" />}
|
height={16}
|
||||||
|
src={user?.picture}
|
||||||
|
alt={user?.name}
|
||||||
|
className="rounded-md"
|
||||||
|
/>
|
||||||
|
<AvatarFallback className="text-xs rounded-md">
|
||||||
|
{user?.name ? (
|
||||||
|
user.name.charAt(0).toUpperCase()
|
||||||
|
) : (
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
)}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<ChevronsUpDown className="h-3 w-3 text-muted-foreground" />
|
<ChevronsUpDown size={16} className="text-muted-foreground mx-2" />
|
||||||
</Button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
<DropdownMenuContent className="w-56 p-0" align="end" forceMount>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-2 px-1 py-1">
|
||||||
<p className="text-sm font-medium leading-none">{user?.name}</p>
|
<p className="text-sm font-medium leading-none">{user?.name}</p>
|
||||||
<p className="text-xs leading-none text-muted-foreground">
|
<p className="text-xs leading-none text-muted-foreground">
|
||||||
{user?.email}
|
{user?.email}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator className="m-0" />
|
||||||
<DropdownMenuItem onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
|
<div className="flex items-center justify-between pl-3 pr-2 h-9">
|
||||||
{theme === "light" ? (
|
<span className="text-sm">Theme</span>
|
||||||
<Moon className="mr-2 h-4 w-4" />
|
<ThemeButtons />
|
||||||
) : (
|
</div>
|
||||||
<Sun className="mr-2 h-4 w-4" />
|
<DropdownMenuSeparator className="m-0" />
|
||||||
)}
|
<button
|
||||||
<span>Toggle Theme</span>
|
onClick={logout}
|
||||||
</DropdownMenuItem>
|
className="flex items-center hover:bg-muted w-full h-9 px-3"
|
||||||
<DropdownMenuSeparator />
|
>
|
||||||
<DropdownMenuItem onClick={logout} className="text-red-600 focus:text-red-600">
|
<LogOut className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<span className="text-sm">Logout</span>
|
||||||
<span>Log out</span>
|
</button>
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +96,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
||||||
const refreshConversations = useCallback((force = false) => {
|
const refreshConversations = useCallback((force = false) => {
|
||||||
if (force) {
|
if (force) {
|
||||||
// Immediate refresh for important updates like new conversations
|
// Immediate refresh for important updates like new conversations
|
||||||
setRefreshTrigger((prev) => prev + 1);
|
setRefreshTrigger(prev => prev + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,7 +107,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
||||||
|
|
||||||
// Set a new timeout to debounce multiple rapid refresh calls
|
// Set a new timeout to debounce multiple rapid refresh calls
|
||||||
refreshTimeoutRef.current = setTimeout(() => {
|
refreshTimeoutRef.current = setTimeout(() => {
|
||||||
setRefreshTrigger((prev) => prev + 1);
|
setRefreshTrigger(prev => prev + 1);
|
||||||
}, 250); // 250ms debounce
|
}, 250); // 250ms debounce
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -123,7 +123,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
||||||
// Silent refresh - updates data without loading states
|
// Silent refresh - updates data without loading states
|
||||||
const refreshConversationsSilent = useCallback(async () => {
|
const refreshConversationsSilent = useCallback(async () => {
|
||||||
// Trigger silent refresh that updates conversation data without showing loading states
|
// Trigger silent refresh that updates conversation data without showing loading states
|
||||||
setRefreshTriggerSilent((prev) => prev + 1);
|
setRefreshTriggerSilent(prev => prev + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadConversation = useCallback((conversation: ConversationData) => {
|
const loadConversation = useCallback((conversation: ConversationData) => {
|
||||||
|
|
@ -164,7 +164,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
||||||
}, [endpoint, refreshConversations]);
|
}, [endpoint, refreshConversations]);
|
||||||
|
|
||||||
const addConversationDoc = useCallback((filename: string) => {
|
const addConversationDoc = useCallback((filename: string) => {
|
||||||
setConversationDocs((prev) => [
|
setConversationDocs(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
{ filename, uploadTime: new Date() },
|
{ filename, uploadTime: new Date() },
|
||||||
]);
|
]);
|
||||||
|
|
@ -180,7 +180,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
||||||
setCurrentConversationId(null); // Clear current conversation to indicate new conversation
|
setCurrentConversationId(null); // Clear current conversation to indicate new conversation
|
||||||
setConversationData(null); // Clear conversation data to prevent reloading
|
setConversationData(null); // Clear conversation data to prevent reloading
|
||||||
// Set the response ID that we're forking from as the previous response ID
|
// Set the response ID that we're forking from as the previous response ID
|
||||||
setPreviousResponseIds((prev) => ({
|
setPreviousResponseIds(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[endpoint]: responseId,
|
[endpoint]: responseId,
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue