Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1ea8046
feat(*): initate chat feature for llm call
Ayush8923 Apr 27, 2026
f9518fb
Merge branch 'main' into feat/chat-llm-call
Ayush8923 Apr 28, 2026
289f06a
fix(*): update the sidebar ordering and few update in primary color
Ayush8923 Apr 28, 2026
9d681b4
fix(*): update the sidebar and their branding
Ayush8923 Apr 28, 2026
fbdbd3e
fix(*): make the branding update and sidebar updates
Ayush8923 Apr 28, 2026
2e740c5
fix(*): update the child submenu with color pellete
Ayush8923 Apr 28, 2026
5c2adfc
fix(chat): cleanups
Ayush8923 Apr 28, 2026
68d2c0a
fix(chat): cleanups
Ayush8923 Apr 28, 2026
919d893
fix(chat): remove the js comments and cleanups
Ayush8923 Apr 28, 2026
44a5b9c
fix(*): remove the unused and unwanted svg
Ayush8923 Apr 28, 2026
9e9f179
fix(chat): remove the js comments and cleanups
Ayush8923 Apr 28, 2026
c2ff835
fix(*): ui updates and clenaups
Ayush8923 Apr 29, 2026
cb83ed0
fix(*): ui updates
Ayush8923 Apr 29, 2026
d577d3d
fix(*): cleanups and refactoring
Ayush8923 Apr 29, 2026
a4c55fb
Merge branch 'main' into feat/chat-llm-call
Ayush8923 Apr 29, 2026
5c84322
fix(*): remove the webhook implementation use the polling mechanism
Ayush8923 Apr 29, 2026
645d1b6
Merge branch 'feat/chat-llm-call' of https://github.com/ProjectTech4D…
Ayush8923 Apr 29, 2026
ef172e1
fix(*): remove the js comments
Ayush8923 Apr 29, 2026
a7f825e
fix(*): update the suggestion prompt
Ayush8923 Apr 29, 2026
817ac6a
fix(*): chat llm refresh store
Ayush8923 Apr 30, 2026
61d3883
fix(*): added the onboarding and back to org button
Ayush8923 Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
BACKEND_URL=http://localhost:8000
GUARDRAILS_URL = http://localhost:8001
GUARDRAILS_TOKEN =
GUARDRAILS_TOKEN =
Comment thread
Ayush8923 marked this conversation as resolved.
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
299 changes: 299 additions & 0 deletions app/(main)/chat/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
/**
* Chat - conversational interface.
*/

"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import Sidebar from "@/app/components/Sidebar";
import PageHeader from "@/app/components/PageHeader";
import { useApp } from "@/app/lib/context/AppContext";
import { useAuth } from "@/app/lib/context/AuthContext";
import { useToast } from "@/app/components/Toast";
import { LoginModal } from "@/app/components/auth";
import {
ChatConfigPicker,
ChatEmptyState,
ChatInput,
ChatMessageList,
} from "@/app/components/chat";
import { useConfigs } from "@/app/hooks";
import {
configToBlob,
createLLMCall,
extractAssistantText,
pollLLMCall,
} from "@/app/lib/chatClient";
import { useChatStore } from "@/app/lib/store/chat";
import { ChatMessage, LLMCallRequest } from "@/app/lib/types/chat";

function genId() {
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
return crypto.randomUUID();
}
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}

export default function ChatPage() {
const { sidebarCollapsed } = useApp();
const { isAuthenticated, activeKey, isHydrated } = useAuth();
const apiKey = activeKey?.key ?? "";
const toast = useToast();
const { configs, loadSingleVersion, allConfigMeta } = useConfigs({
pageSize: 0,
});

const messages = useChatStore((s) => s.messages);
const conversationId = useChatStore((s) => s.conversationId);
const configId = useChatStore((s) => s.configId);
const configVersion = useChatStore((s) => s.configVersion);
const chatHydrated = useChatStore((s) => s.hasHydrated);
const appendMessages = useChatStore((s) => s.appendMessages);
const updateMessageInStore = useChatStore((s) => s.updateMessage);
const setConversationId = useChatStore((s) => s.setConversationId);
const setConfig = useChatStore((s) => s.setConfig);
const clearConversation = useChatStore((s) => s.clearConversation);

const [draft, setDraft] = useState("");
const [isPending, setIsPending] = useState(false);
const [showLoginModal, setShowLoginModal] = useState(false);

const abortRef = useRef<AbortController | null>(null);

// Trigger persisted-state rehydration on mount.
useEffect(() => {
useChatStore.persist.rehydrate();
}, []);

// Cancel any in-flight poll when leaving the page.
useEffect(() => {
return () => abortRef.current?.abort();
}, []);

const handleNewChat = useCallback(() => {
abortRef.current?.abort();
abortRef.current = null;
clearConversation();
setIsPending(false);
}, [clearConversation]);

const handleConfigSelect = useCallback(
(newConfigId: string, newVersion: number) => {
const isDifferent =
newConfigId !== configId || newVersion !== configVersion;
setConfig(newConfigId, newVersion);
if (isDifferent) {
clearConversation();
}
},
[clearConversation, configId, configVersion, setConfig],
);

const sendMessage = useCallback(
async (text: string) => {
const trimmed = text.trim();
if (!trimmed) return;

if (!isAuthenticated) {
setShowLoginModal(true);
return;
}
if (!configId || !configVersion) {
if (allConfigMeta.length === 0) {
toast.error(
"No configurations yet — create one in Configurations → Prompt Editor first.",
);
} else {
toast.error("Select a configuration before sending a message.");
}
return;
}

const userMessage: ChatMessage = {
id: genId(),
role: "user",
content: trimmed,
createdAt: Date.now(),
status: "complete",
};
const assistantMessage: ChatMessage = {
id: genId(),
role: "assistant",
content: "",
createdAt: Date.now(),
status: "pending",
};

appendMessages(userMessage, assistantMessage);
setDraft("");
setIsPending(true);

const controller = new AbortController();
abortRef.current?.abort();
abortRef.current = controller;

try {
const cached = configs.find(
(c) => c.config_id === configId && c.version === configVersion,
);
const fullConfig =
cached ?? (await loadSingleVersion(configId, configVersion));
if (!fullConfig) {
throw new Error(
"Couldn't load the selected configuration. Try picking it again.",
);
}

const payload: LLMCallRequest = {
query: {
input: trimmed,
conversation: conversationId
? { id: conversationId }
: { auto_create: true },
},
config: { blob: configToBlob(fullConfig) },
include_provider_raw_response: true,
};

const created = await createLLMCall(payload, apiKey);
if (!created.success || !created.data?.job_id) {
throw new Error(created.error || "Failed to start the request");
}
const jobId = created.data.job_id;
updateMessageInStore(assistantMessage.id, { jobId });

const result = await pollLLMCall(jobId, apiKey, {
signal: controller.signal,
});

const text = extractAssistantText(result.llm_response?.response);
const newConversationId =
result.llm_response?.response?.conversation_id ?? conversationId;
if (newConversationId && newConversationId !== conversationId) {
setConversationId(newConversationId);
}

updateMessageInStore(assistantMessage.id, {
content:
text ||
"(The assistant returned an empty response — try again or pick a different configuration.)",
status: "complete",
});
} catch (err) {
if ((err as Error)?.name === "AbortError") {
updateMessageInStore(assistantMessage.id, {
status: "error",
content: "Cancelled.",
error: "Cancelled",
});
return;
}
const message =
err instanceof Error ? err.message : "Something went wrong";
updateMessageInStore(assistantMessage.id, {
status: "error",
content: message,
error: message,
});
toast.error(message);
} finally {
if (abortRef.current === controller) {
abortRef.current = null;
}
setIsPending(false);
}
},
[
allConfigMeta,
apiKey,
appendMessages,
configId,
configVersion,
configs,
conversationId,
isAuthenticated,
loadSingleVersion,
setConversationId,
toast,
updateMessageInStore,
],
);

const hasConversation = messages.length > 0;
const hasConfig = !!configId && !!configVersion;
const isReady = isHydrated && chatHydrated;

return (
<div className="w-full h-screen flex flex-col bg-bg-secondary">
<div className="flex flex-1 overflow-hidden">
<Sidebar collapsed={sidebarCollapsed} activeRoute="/chat" />

<div className="flex-1 flex flex-col overflow-hidden bg-bg-primary">
<PageHeader
title="Chat"
subtitle="Ask anything - answers come from your selected configuration"
actions={
hasConversation ? (
<button
type="button"
onClick={handleNewChat}
className="px-3 py-1.5 rounded-full text-xs font-medium border border-border bg-bg-primary text-text-primary hover:bg-neutral-50 transition-colors cursor-pointer"
>
New chat
</button>
) : null
}
/>

{!isReady ? (
<div className="flex-1" />
) : hasConversation ? (
<ChatMessageList messages={messages} />
) : (
<ChatEmptyState
hasConfig={hasConfig}
isAuthenticated={isAuthenticated}
onSuggestion={(text) => {
if (!isAuthenticated) {
setShowLoginModal(true);
return;
}
sendMessage(text);
}}
/>
)}

<ChatInput
value={draft}
onChange={setDraft}
onSend={() => sendMessage(draft)}
isPending={isPending}
placeholder={
!isAuthenticated
? "Log in to start chatting…"
: !hasConfig
? "Select a configuration to start chatting…"
: "Message your assistant…"
}
trailingAccessory={
isAuthenticated ? (
<ChatConfigPicker
configId={configId}
version={configVersion}
onSelect={handleConfigSelect}
disabled={isPending}
openUp
/>
) : null
}
/>
</div>
</div>

<LoginModal
open={showLoginModal}
onClose={() => setShowLoginModal(false)}
/>
</div>
);
}
2 changes: 1 addition & 1 deletion app/(main)/document/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function DocumentPage() {
/>

<div className="flex-1 overflow-hidden flex bg-bg-secondary">
<div className="w-1/3 border-r overflow-hidden border-[hsl(0,0%,85%)]">
<div className="w-1/3 border-r border-r-status-default-border overflow-hidden">
<DocumentListing
documents={documents}
selectedDocument={selectedDocument}
Expand Down
4 changes: 2 additions & 2 deletions app/(main)/settings/credentials/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export default function CredentialsPage() {
};

return (
<div className="w-full h-screen flex flex-col bg-bg-secondary">
<div className="w-full h-screen flex flex-col bg-bg-primary">
<div className="flex flex-1 overflow-hidden">
<SettingsSidebar />

Expand All @@ -192,7 +192,7 @@ export default function CredentialsPage() {
selectedProvider={selectedProvider}
credentials={credentials}
onSelect={setSelectedProvider}
className="w-56 border-r border-border overflow-y-auto bg-bg-secondary"
className="w-56 border-r border-border overflow-y-auto bg-bg-primary"
/>

<div className="flex-1 overflow-y-auto p-8">
Expand Down
20 changes: 18 additions & 2 deletions app/(main)/settings/onboarding/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export default function OnboardingPage() {
isLoadingMore,
hasMore,
loadMore,
refetch: refetchOrganizations,
} = usePaginatedList<Organization>({
endpoint: "/api/organization",
limit: DEFAULT_PAGE_LIMIT,
Expand Down Expand Up @@ -147,6 +148,17 @@ export default function OnboardingPage() {
setView("success");
};

const handleOnboardAnother = () => {
setOnboardData(null);
setView("form");
};

const handleBackToOrgsFromSuccess = () => {
setOnboardData(null);
refetchOrganizations();
setView("list");
};

const handleSelectProject = (project: Project) => {
setSelectedProject(project);
setView("users");
Expand All @@ -165,7 +177,7 @@ export default function OnboardingPage() {
};

return (
<div className="w-full h-screen flex flex-col bg-bg-secondary">
<div className="w-full h-screen flex flex-col bg-bg-primary">
<div className="flex flex-1 overflow-hidden">
<SettingsSidebar />

Expand Down Expand Up @@ -293,7 +305,11 @@ export default function OnboardingPage() {
/>
</div>

<OnboardingSuccess data={onboardData} />
<OnboardingSuccess
data={onboardData}
onOnboardAnother={handleOnboardAnother}
onBackToList={handleBackToOrgsFromSuccess}
/>
</>
)}
</div>
Expand Down
Loading
Loading