mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-16 23:08:38 +02:00
Add prototypes folder to test new functionality in (#6081)
# Description of Changes Add prototypes folder to test new functionality in. This build of the app is spawnable with `npm run dev:prototypes`. Currently just contains a very developer-y chat interface to help us develop & explore the AI backend before we make the frontend for it for real.
This commit is contained in:
2
LICENSE
2
LICENSE
@@ -14,6 +14,8 @@ if that directory exists, is licensed under the license defined in "frontend/src
|
||||
if that directory exists, is licensed under the license defined in "frontend/src/desktop/LICENSE".
|
||||
* All content that resides under the "frontend/src/saas/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/src/saas/LICENSE".
|
||||
* All content that resides under the "frontend/src/prototypes/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/src/prototypes/LICENSE".
|
||||
* Content outside of the above mentioned directories or restrictions above is
|
||||
available under the MIT License as defined below.
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ export default defineConfig(
|
||||
files: [
|
||||
'src/proprietary/**/*.{js,mjs,jsx,ts,tsx}',
|
||||
'src/saas/**/*.{js,mjs,jsx,ts,tsx}',
|
||||
'src/prototypes/**/*.{js,mjs,jsx,ts,tsx}',
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
"dev:proprietary": "npm run prep && vite --mode proprietary",
|
||||
"dev:saas": "npm run prep:saas && vite --mode saas",
|
||||
"dev:desktop": "npm run prep:desktop && vite --mode desktop",
|
||||
"dev:prototypes": "npm run prep && vite --mode prototypes",
|
||||
"lint": "npm run lint:eslint && npm run lint:cycles",
|
||||
"lint:eslint": "eslint --max-warnings=0",
|
||||
"lint:cycles": "dpdm src --circular --no-warning --no-tree --exit-code circular:1",
|
||||
@@ -96,6 +97,7 @@
|
||||
"build:proprietary": "npm run prep && vite build --mode proprietary",
|
||||
"build:saas": "npm run prep:saas && vite build --mode saas",
|
||||
"build:desktop": "npm run prep:desktop && vite build --mode desktop",
|
||||
"build:prototypes": "npm run prep && vite build --mode prototypes",
|
||||
"preview": "vite preview",
|
||||
"tauri-dev": "npm run prep:desktop && tauri dev --no-watch",
|
||||
"tauri-build": "npm run prep:desktop-build && tauri build",
|
||||
@@ -110,8 +112,9 @@
|
||||
"typecheck:proprietary": "tsc --noEmit --project src/proprietary/tsconfig.json",
|
||||
"typecheck:saas": "tsc --noEmit --project src/saas/tsconfig.json",
|
||||
"typecheck:desktop": "tsc --noEmit --project src/desktop/tsconfig.json",
|
||||
"typecheck:prototypes": "tsc --noEmit --project src/prototypes/tsconfig.json",
|
||||
"typecheck:scripts": "tsc --noEmit --project scripts/tsconfig.json",
|
||||
"typecheck:all": "npm run typecheck:core && npm run typecheck:proprietary && npm run typecheck:saas && npm run typecheck:desktop && npm run typecheck:scripts",
|
||||
"typecheck:all": "npm run typecheck:core && npm run typecheck:proprietary && npm run typecheck:saas && npm run typecheck:desktop && npm run typecheck:prototypes && npm run typecheck:scripts",
|
||||
"check": "npm run typecheck && npm run lint && npm run test:run",
|
||||
"generate-licenses": "node scripts/generate-licenses.js",
|
||||
"generate-icons": "node scripts/generate-icons.js",
|
||||
|
||||
81
frontend/src/prototypes/App.tsx
Normal file
81
frontend/src/prototypes/App.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Suspense } from "react";
|
||||
import { Routes, Route, useParams } from "react-router-dom";
|
||||
import { AppProviders } from "@app/components/AppProviders";
|
||||
import { AppLayout } from "@app/components/AppLayout";
|
||||
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
|
||||
import { PreferencesProvider } from "@app/contexts/PreferencesContext";
|
||||
import { RainbowThemeProvider } from "@app/components/shared/RainbowThemeProvider";
|
||||
import Landing from "@app/routes/Landing";
|
||||
import Login from "@app/routes/Login";
|
||||
import Signup from "@app/routes/Signup";
|
||||
import AuthCallback from "@app/routes/AuthCallback";
|
||||
import InviteAccept from "@app/routes/InviteAccept";
|
||||
import ShareLinkPage from "@app/routes/ShareLinkPage";
|
||||
import ParticipantView from "@app/components/workflow/ParticipantView";
|
||||
import Onboarding from "@app/components/onboarding/Onboarding";
|
||||
|
||||
// Import global styles
|
||||
import "@app/styles/tailwind.css";
|
||||
import "@app/styles/cookieconsent.css";
|
||||
import "@app/styles/index.css";
|
||||
import "@app/styles/auth-theme.css";
|
||||
|
||||
// Import file ID debugging helpers (development only)
|
||||
import "@app/utils/fileIdSafety";
|
||||
|
||||
// Minimal providers for public routes - no API calls, no authentication
|
||||
function MinimalProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<PreferencesProvider>
|
||||
<RainbowThemeProvider>
|
||||
{children}
|
||||
</RainbowThemeProvider>
|
||||
</PreferencesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Participant signing page — token-gated, no login required
|
||||
function ParticipantViewPage() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
if (!token) return null;
|
||||
return <ParticipantView token={token} />;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Routes>
|
||||
{/* Participant signing — public, token-gated, no auth required */}
|
||||
<Route
|
||||
path="/workflow/sign/:token"
|
||||
element={
|
||||
<MinimalProviders>
|
||||
<ParticipantViewPage />
|
||||
</MinimalProviders>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* All other routes need AppProviders for backend integration */}
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<AppProviders>
|
||||
<AppLayout>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/signup" element={<Signup />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
<Route path="/invite/:token" element={<InviteAccept />} />
|
||||
<Route path="/share/:token" element={<ShareLinkPage />} />
|
||||
{/* Main app routes - Landing handles auth logic */}
|
||||
<Route path="/*" element={<Landing />} />
|
||||
</Routes>
|
||||
<Onboarding />
|
||||
</AppLayout>
|
||||
</AppProviders>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
51
frontend/src/prototypes/LICENSE
Normal file
51
frontend/src/prototypes/LICENSE
Normal file
@@ -0,0 +1,51 @@
|
||||
Stirling PDF User License
|
||||
|
||||
Copyright (c) 2025 Stirling PDF Inc.
|
||||
|
||||
License Scope & Usage Rights
|
||||
|
||||
Production use of the Stirling PDF Software is only permitted with a valid Stirling PDF User License.
|
||||
|
||||
For purposes of this license, “the Software” refers to the Stirling PDF application and any associated documentation files
|
||||
provided by Stirling PDF Inc. You or your organization may not use the Software in production, at scale, or for business-critical
|
||||
processes unless you have agreed to, and remain in compliance with, the Stirling PDF Subscription Terms of Service
|
||||
(https://www.stirlingpdf.com/terms) or another valid agreement with Stirling PDF, and hold an active User License subscription
|
||||
covering the appropriate number of licensed users.
|
||||
|
||||
Trial and Minimal Use
|
||||
|
||||
You may use the Software without a paid subscription for the sole purposes of internal trial, evaluation, or minimal use, provided that:
|
||||
* Use is limited to the capabilities and restrictions defined by the Software itself;
|
||||
* You do not copy, distribute, sublicense, reverse-engineer, or use the Software in client-facing or commercial contexts.
|
||||
|
||||
Continued use beyond this scope requires a valid Stirling PDF User License.
|
||||
|
||||
Modifications and Derivative Works
|
||||
|
||||
You may modify the Software only for development or internal testing purposes. Any such modifications or derivative works:
|
||||
|
||||
* May not be deployed in production environments without a valid User License;
|
||||
* May not be distributed or sublicensed;
|
||||
* Remain the intellectual property of Stirling PDF and/or its licensors;
|
||||
* May only be used, copied, or exploited in accordance with the terms of a valid Stirling PDF User License subscription.
|
||||
|
||||
Prohibited Actions
|
||||
|
||||
Unless explicitly permitted by a paid license or separate agreement, you may not:
|
||||
|
||||
* Use the Software in production environments;
|
||||
* Copy, merge, distribute, sublicense, or sell the Software;
|
||||
* Remove or alter any licensing or copyright notices;
|
||||
* Circumvent access restrictions or licensing requirements.
|
||||
|
||||
Third-Party Components
|
||||
|
||||
The Stirling PDF Software may include components subject to separate open source licenses. Such components remain governed by
|
||||
their original license terms as provided by their respective owners.
|
||||
|
||||
Disclaimer
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
18
frontend/src/prototypes/components/AppProviders.tsx
Normal file
18
frontend/src/prototypes/components/AppProviders.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders";
|
||||
import { type AppProvidersProps } from "@core/components/AppProviders";
|
||||
import { ChatProvider } from "@app/components/chat/ChatContext";
|
||||
|
||||
export type { AppProvidersProps };
|
||||
|
||||
export function AppProviders({ children, appConfigRetryOptions, appConfigProviderProps }: AppProvidersProps) {
|
||||
return (
|
||||
<ProprietaryAppProviders
|
||||
appConfigRetryOptions={appConfigRetryOptions}
|
||||
appConfigProviderProps={appConfigProviderProps}
|
||||
>
|
||||
<ChatProvider>
|
||||
{children}
|
||||
</ChatProvider>
|
||||
</ProprietaryAppProviders>
|
||||
);
|
||||
}
|
||||
180
frontend/src/prototypes/components/chat/ChatContext.tsx
Normal file
180
frontend/src/prototypes/components/chat/ChatContext.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { createContext, useContext, useReducer, useCallback, type ReactNode } from "react";
|
||||
import { useAllFiles } from "@app/contexts/FileContext";
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
type AiWorkflowOutcome =
|
||||
| "answer"
|
||||
| "not_found"
|
||||
| "need_content"
|
||||
| "plan"
|
||||
| "need_clarification"
|
||||
| "cannot_do"
|
||||
| "tool_call"
|
||||
| "completed"
|
||||
| "unsupported_capability"
|
||||
| "cannot_continue";
|
||||
|
||||
interface AiWorkflowResponse {
|
||||
outcome: AiWorkflowOutcome;
|
||||
answer?: string;
|
||||
summary?: string;
|
||||
rationale?: string;
|
||||
reason?: string;
|
||||
question?: string;
|
||||
capability?: string;
|
||||
message?: string;
|
||||
evidence?: Array<{ pageNumber: number; text: string }>;
|
||||
steps?: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
interface ChatState {
|
||||
messages: ChatMessage[];
|
||||
isOpen: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
type ChatAction =
|
||||
| { type: "ADD_MESSAGE"; message: ChatMessage }
|
||||
| { type: "SET_LOADING"; loading: boolean }
|
||||
| { type: "TOGGLE_OPEN" }
|
||||
| { type: "SET_OPEN"; open: boolean };
|
||||
|
||||
function chatReducer(state: ChatState, action: ChatAction): ChatState {
|
||||
switch (action.type) {
|
||||
case "ADD_MESSAGE":
|
||||
return { ...state, messages: [...state.messages, action.message] };
|
||||
case "SET_LOADING":
|
||||
return { ...state, isLoading: action.loading };
|
||||
case "TOGGLE_OPEN":
|
||||
return { ...state, isOpen: !state.isOpen };
|
||||
case "SET_OPEN":
|
||||
return { ...state, isOpen: action.open };
|
||||
}
|
||||
}
|
||||
|
||||
function formatWorkflowResponse(data: AiWorkflowResponse): string {
|
||||
switch (data.outcome) {
|
||||
case "answer":
|
||||
case "completed":
|
||||
return data.answer ?? data.summary ?? "Done.";
|
||||
case "need_clarification":
|
||||
return data.question ?? "Could you clarify your request?";
|
||||
case "cannot_do":
|
||||
return data.reason ?? "I'm unable to do that.";
|
||||
case "not_found":
|
||||
return data.reason ?? "I couldn't find the requested information.";
|
||||
case "unsupported_capability":
|
||||
return data.message ?? `Unsupported capability: ${data.capability ?? "unknown"}`;
|
||||
case "cannot_continue":
|
||||
return data.reason ?? "Something went wrong and I can't continue.";
|
||||
case "plan":
|
||||
return data.rationale
|
||||
? `${data.rationale}\n\n${(data.steps ?? []).map((s, i) => `${i + 1}. ${JSON.stringify(s)}`).join("\n")}`
|
||||
: JSON.stringify(data.steps, null, 2);
|
||||
case "need_content":
|
||||
case "tool_call":
|
||||
return data.rationale ?? data.summary ?? `Processing (${data.outcome})...`;
|
||||
default:
|
||||
return data.answer ?? data.summary ?? data.message ?? JSON.stringify(data);
|
||||
}
|
||||
}
|
||||
|
||||
interface ChatContextValue {
|
||||
messages: ChatMessage[];
|
||||
isOpen: boolean;
|
||||
isLoading: boolean;
|
||||
toggleOpen: () => void;
|
||||
setOpen: (open: boolean) => void;
|
||||
sendMessage: (content: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ChatContextValue | null>(null);
|
||||
|
||||
const initialState: ChatState = {
|
||||
messages: [],
|
||||
isOpen: false,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
export function ChatProvider({ children }: { children: ReactNode }) {
|
||||
const [state, dispatch] = useReducer(chatReducer, initialState);
|
||||
const { files: activeFiles } = useAllFiles();
|
||||
|
||||
const toggleOpen = useCallback(() => dispatch({ type: "TOGGLE_OPEN" }), []);
|
||||
const setOpen = useCallback((open: boolean) => dispatch({ type: "SET_OPEN", open }), []);
|
||||
|
||||
const sendMessage = useCallback(async (content: string) => {
|
||||
const userMessage: ChatMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
role: "user",
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
dispatch({ type: "ADD_MESSAGE", message: userMessage });
|
||||
dispatch({ type: "SET_LOADING", loading: true });
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("userMessage", content);
|
||||
activeFiles.forEach((file, i) => {
|
||||
formData.append(`fileInputs[${i}].fileInput`, file);
|
||||
});
|
||||
|
||||
const response = await fetch("/api/v1/ai/orchestrate", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`AI engine request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: AiWorkflowResponse = await response.json();
|
||||
const replyContent = formatWorkflowResponse(data);
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
role: "assistant",
|
||||
content: replyContent,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
dispatch({ type: "ADD_MESSAGE", message: assistantMessage });
|
||||
} catch {
|
||||
const errorMessage: ChatMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
role: "assistant",
|
||||
content: "Failed to get a response. The AI engine may not be available yet.",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
dispatch({ type: "ADD_MESSAGE", message: errorMessage });
|
||||
} finally {
|
||||
dispatch({ type: "SET_LOADING", loading: false });
|
||||
}
|
||||
}, [activeFiles]);
|
||||
|
||||
return (
|
||||
<ChatContext.Provider value={{
|
||||
messages: state.messages,
|
||||
isOpen: state.isOpen,
|
||||
isLoading: state.isLoading,
|
||||
toggleOpen,
|
||||
setOpen,
|
||||
sendMessage,
|
||||
}}>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useChat(): ChatContextValue {
|
||||
const context = useContext(ChatContext);
|
||||
if (!context) {
|
||||
throw new Error("useChat must be used within a ChatProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
74
frontend/src/prototypes/components/chat/ChatPanel.css
Normal file
74
frontend/src/prototypes/components/chat/ChatPanel.css
Normal file
@@ -0,0 +1,74 @@
|
||||
.chat-toggle-button {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 380px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--mantine-color-body);
|
||||
border-left: 1px solid var(--border-subtle, var(--mantine-color-default-border));
|
||||
z-index: 999;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chat-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-subtle, var(--mantine-color-default-border));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-panel-messages {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.chat-panel-input {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border-top: 1px solid var(--border-subtle, var(--mantine-color-default-border));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Message layout */
|
||||
.chat-message {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chat-message-user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chat-message-assistant {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Bubble styling */
|
||||
.chat-bubble {
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.chat-bubble-user {
|
||||
background: var(--mantine-color-blue-filled) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.chat-bubble-user * {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.chat-bubble-assistant {
|
||||
background: var(--mantine-color-default-hover) !important;
|
||||
}
|
||||
135
frontend/src/prototypes/components/chat/ChatPanel.tsx
Normal file
135
frontend/src/prototypes/components/chat/ChatPanel.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { useRef, useEffect, useState, type KeyboardEvent } from "react";
|
||||
import { ActionIcon, ScrollArea, TextInput, Stack, Text, Paper, Box, Transition } from "@mantine/core";
|
||||
import SendIcon from "@mui/icons-material/Send";
|
||||
import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useChat } from "@app/components/chat/ChatContext";
|
||||
import "@app/components/chat/ChatPanel.css";
|
||||
|
||||
function ChatMessageBubble({ role, content }: { role: "user" | "assistant"; content: string }) {
|
||||
return (
|
||||
<div className={`chat-message chat-message-${role}`}>
|
||||
<Paper
|
||||
className={`chat-bubble chat-bubble-${role}`}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<Text size="sm" style={{ whiteSpace: "pre-wrap" }}>{content}</Text>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatPanel() {
|
||||
const { messages, isOpen, isLoading, toggleOpen, sendMessage } = useChat();
|
||||
const [input, setInput] = useState("");
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" });
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleSend = () => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed || isLoading) return;
|
||||
setInput("");
|
||||
sendMessage(trimmed);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Toggle button - always visible */}
|
||||
{!isOpen && (
|
||||
<ActionIcon
|
||||
className="chat-toggle-button"
|
||||
variant="filled"
|
||||
color="blue"
|
||||
size="xl"
|
||||
radius="xl"
|
||||
onClick={toggleOpen}
|
||||
aria-label="Open chat"
|
||||
>
|
||||
<ChatBubbleOutlineIcon sx={{ fontSize: 24 }} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
||||
{/* Chat panel */}
|
||||
<Transition mounted={isOpen} transition="slide-left" duration={200}>
|
||||
{(styles) => (
|
||||
<Box className="chat-panel" style={styles}>
|
||||
{/* Header */}
|
||||
<div className="chat-panel-header">
|
||||
<Text fw={600} size="sm">AI Assistant</Text>
|
||||
<ActionIcon variant="subtle" size="sm" onClick={toggleOpen} aria-label="Close chat">
|
||||
<CloseIcon sx={{ fontSize: 16 }} />
|
||||
</ActionIcon>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<ScrollArea className="chat-panel-messages" viewportRef={scrollRef}>
|
||||
<Stack gap="sm" p="sm">
|
||||
{messages.length === 0 && (
|
||||
<Text size="sm" c="dimmed" ta="center" py="xl">
|
||||
Ask a question about your documents or get help with PDF tools.
|
||||
</Text>
|
||||
)}
|
||||
{messages.map((msg) => (
|
||||
<ChatMessageBubble key={msg.id} role={msg.role} content={msg.content} />
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="chat-message chat-message-assistant">
|
||||
<Paper className="chat-bubble chat-bubble-assistant" p="xs" radius="md">
|
||||
<Text size="sm" c="dimmed">Thinking...</Text>
|
||||
</Paper>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Input */}
|
||||
<div className="chat-panel-input">
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
placeholder="Type a message..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.currentTarget.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isLoading}
|
||||
rightSection={
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="blue"
|
||||
size="sm"
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || isLoading}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<SendIcon sx={{ fontSize: 14 }} />
|
||||
</ActionIcon>
|
||||
}
|
||||
rightSectionWidth={36}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ChatPanel } from "@app/components/chat/ChatPanel";
|
||||
|
||||
export function HomePageExtensions() {
|
||||
return <ChatPanel />;
|
||||
}
|
||||
27
frontend/src/prototypes/tsconfig.json
Normal file
27
frontend/src/prototypes/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "../../",
|
||||
"paths": {
|
||||
"@app/*": [
|
||||
"src/prototypes/*",
|
||||
"src/proprietary/*",
|
||||
"src/core/*"
|
||||
],
|
||||
"@proprietary/*": [
|
||||
"src/proprietary/*"
|
||||
],
|
||||
"@core/*": [
|
||||
"src/core/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"../global.d.ts",
|
||||
"../*.js",
|
||||
"../*.ts",
|
||||
"../*.tsx",
|
||||
"../core/setupTests.ts",
|
||||
"."
|
||||
]
|
||||
}
|
||||
23
frontend/tsconfig.prototypes.vite.json
Normal file
23
frontend/tsconfig.prototypes.vite.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "./tsconfig.proprietary.vite.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@app/*": [
|
||||
"src/prototypes/*",
|
||||
"src/proprietary/*",
|
||||
"src/core/*"
|
||||
],
|
||||
"@proprietary/*": ["src/proprietary/*"],
|
||||
"@core/*": ["src/core/*"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"src/core/**/*.test.ts*",
|
||||
"src/core/**/*.spec.ts*",
|
||||
"src/proprietary/**/*.test.ts*",
|
||||
"src/proprietary/**/*.spec.ts*",
|
||||
"src/desktop",
|
||||
"src/saas",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react-swc';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||
|
||||
const VALID_MODES = ['core', 'proprietary', 'saas', 'desktop'] as const;
|
||||
const VALID_MODES = ['core', 'proprietary', 'saas', 'desktop', 'prototypes'] as const;
|
||||
type BuildMode = typeof VALID_MODES[number];
|
||||
|
||||
const TSCONFIG_MAP: Record<BuildMode, string> = {
|
||||
@@ -11,6 +11,7 @@ const TSCONFIG_MAP: Record<BuildMode, string> = {
|
||||
proprietary: './tsconfig.proprietary.vite.json',
|
||||
saas: './tsconfig.saas.vite.json',
|
||||
desktop: './tsconfig.desktop.vite.json',
|
||||
prototypes: './tsconfig.prototypes.vite.json',
|
||||
};
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
|
||||
@@ -100,6 +100,24 @@ export default defineConfig({
|
||||
target: 'es2020'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: {
|
||||
name: 'prototypes',
|
||||
include: ['src/prototypes/**/*.test.{ts,tsx}'],
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./src/core/setupTests.ts'],
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
tsconfigPaths({
|
||||
projects: ['./tsconfig.prototypes.vite.json'],
|
||||
}),
|
||||
],
|
||||
esbuild: {
|
||||
target: 'es2020'
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
esbuild: {
|
||||
|
||||
Reference in New Issue
Block a user