Skip to content

Commit cc1604a

Browse files
authored
Add prototypes folder to test new functionality in (Stirling-Tools#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.
1 parent b130242 commit cc1604a

14 files changed

Lines changed: 621 additions & 2 deletions

LICENSE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ if that directory exists, is licensed under the license defined in "frontend/src
1414
if that directory exists, is licensed under the license defined in "frontend/src/desktop/LICENSE".
1515
* All content that resides under the "frontend/src/saas/" directory of this repository,
1616
if that directory exists, is licensed under the license defined in "frontend/src/saas/LICENSE".
17+
* All content that resides under the "frontend/src/prototypes/" directory of this repository,
18+
if that directory exists, is licensed under the license defined in "frontend/src/prototypes/LICENSE".
1719
* Content outside of the above mentioned directories or restrictions above is
1820
available under the MIT License as defined below.
1921

frontend/eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export default defineConfig(
8686
files: [
8787
'src/proprietary/**/*.{js,mjs,jsx,ts,tsx}',
8888
'src/saas/**/*.{js,mjs,jsx,ts,tsx}',
89+
'src/prototypes/**/*.{js,mjs,jsx,ts,tsx}',
8990
],
9091
languageOptions: {
9192
parserOptions: {

frontend/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"dev:proprietary": "npm run prep && vite --mode proprietary",
8989
"dev:saas": "npm run prep:saas && vite --mode saas",
9090
"dev:desktop": "npm run prep:desktop && vite --mode desktop",
91+
"dev:prototypes": "npm run prep && vite --mode prototypes",
9192
"lint": "npm run lint:eslint && npm run lint:cycles",
9293
"lint:eslint": "eslint --max-warnings=0",
9394
"lint:cycles": "dpdm src --circular --no-warning --no-tree --exit-code circular:1",
@@ -96,6 +97,7 @@
9697
"build:proprietary": "npm run prep && vite build --mode proprietary",
9798
"build:saas": "npm run prep:saas && vite build --mode saas",
9899
"build:desktop": "npm run prep:desktop && vite build --mode desktop",
100+
"build:prototypes": "npm run prep && vite build --mode prototypes",
99101
"preview": "vite preview",
100102
"tauri-dev": "npm run prep:desktop && tauri dev --no-watch",
101103
"tauri-build": "npm run prep:desktop-build && tauri build",
@@ -110,8 +112,9 @@
110112
"typecheck:proprietary": "tsc --noEmit --project src/proprietary/tsconfig.json",
111113
"typecheck:saas": "tsc --noEmit --project src/saas/tsconfig.json",
112114
"typecheck:desktop": "tsc --noEmit --project src/desktop/tsconfig.json",
115+
"typecheck:prototypes": "tsc --noEmit --project src/prototypes/tsconfig.json",
113116
"typecheck:scripts": "tsc --noEmit --project scripts/tsconfig.json",
114-
"typecheck:all": "npm run typecheck:core && npm run typecheck:proprietary && npm run typecheck:saas && npm run typecheck:desktop && npm run typecheck:scripts",
117+
"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",
115118
"check": "npm run typecheck && npm run lint && npm run test:run",
116119
"generate-licenses": "node scripts/generate-licenses.js",
117120
"generate-icons": "node scripts/generate-icons.js",

frontend/src/prototypes/App.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Suspense } from "react";
2+
import { Routes, Route, useParams } from "react-router-dom";
3+
import { AppProviders } from "@app/components/AppProviders";
4+
import { AppLayout } from "@app/components/AppLayout";
5+
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
6+
import { PreferencesProvider } from "@app/contexts/PreferencesContext";
7+
import { RainbowThemeProvider } from "@app/components/shared/RainbowThemeProvider";
8+
import Landing from "@app/routes/Landing";
9+
import Login from "@app/routes/Login";
10+
import Signup from "@app/routes/Signup";
11+
import AuthCallback from "@app/routes/AuthCallback";
12+
import InviteAccept from "@app/routes/InviteAccept";
13+
import ShareLinkPage from "@app/routes/ShareLinkPage";
14+
import ParticipantView from "@app/components/workflow/ParticipantView";
15+
import Onboarding from "@app/components/onboarding/Onboarding";
16+
17+
// Import global styles
18+
import "@app/styles/tailwind.css";
19+
import "@app/styles/cookieconsent.css";
20+
import "@app/styles/index.css";
21+
import "@app/styles/auth-theme.css";
22+
23+
// Import file ID debugging helpers (development only)
24+
import "@app/utils/fileIdSafety";
25+
26+
// Minimal providers for public routes - no API calls, no authentication
27+
function MinimalProviders({ children }: { children: React.ReactNode }) {
28+
return (
29+
<PreferencesProvider>
30+
<RainbowThemeProvider>
31+
{children}
32+
</RainbowThemeProvider>
33+
</PreferencesProvider>
34+
);
35+
}
36+
37+
// Participant signing page — token-gated, no login required
38+
function ParticipantViewPage() {
39+
const { token } = useParams<{ token: string }>();
40+
if (!token) return null;
41+
return <ParticipantView token={token} />;
42+
}
43+
44+
export default function App() {
45+
return (
46+
<Suspense fallback={<LoadingFallback />}>
47+
<Routes>
48+
{/* Participant signing — public, token-gated, no auth required */}
49+
<Route
50+
path="/workflow/sign/:token"
51+
element={
52+
<MinimalProviders>
53+
<ParticipantViewPage />
54+
</MinimalProviders>
55+
}
56+
/>
57+
58+
{/* All other routes need AppProviders for backend integration */}
59+
<Route
60+
path="*"
61+
element={
62+
<AppProviders>
63+
<AppLayout>
64+
<Routes>
65+
<Route path="/login" element={<Login />} />
66+
<Route path="/signup" element={<Signup />} />
67+
<Route path="/auth/callback" element={<AuthCallback />} />
68+
<Route path="/invite/:token" element={<InviteAccept />} />
69+
<Route path="/share/:token" element={<ShareLinkPage />} />
70+
{/* Main app routes - Landing handles auth logic */}
71+
<Route path="/*" element={<Landing />} />
72+
</Routes>
73+
<Onboarding />
74+
</AppLayout>
75+
</AppProviders>
76+
}
77+
/>
78+
</Routes>
79+
</Suspense>
80+
);
81+
}

frontend/src/prototypes/LICENSE

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
Stirling PDF User License
2+
3+
Copyright (c) 2025 Stirling PDF Inc.
4+
5+
License Scope & Usage Rights
6+
7+
Production use of the Stirling PDF Software is only permitted with a valid Stirling PDF User License.
8+
9+
For purposes of this license, “the Software” refers to the Stirling PDF application and any associated documentation files
10+
provided by Stirling PDF Inc. You or your organization may not use the Software in production, at scale, or for business-critical
11+
processes unless you have agreed to, and remain in compliance with, the Stirling PDF Subscription Terms of Service
12+
(https://www.stirlingpdf.com/terms) or another valid agreement with Stirling PDF, and hold an active User License subscription
13+
covering the appropriate number of licensed users.
14+
15+
Trial and Minimal Use
16+
17+
You may use the Software without a paid subscription for the sole purposes of internal trial, evaluation, or minimal use, provided that:
18+
* Use is limited to the capabilities and restrictions defined by the Software itself;
19+
* You do not copy, distribute, sublicense, reverse-engineer, or use the Software in client-facing or commercial contexts.
20+
21+
Continued use beyond this scope requires a valid Stirling PDF User License.
22+
23+
Modifications and Derivative Works
24+
25+
You may modify the Software only for development or internal testing purposes. Any such modifications or derivative works:
26+
27+
* May not be deployed in production environments without a valid User License;
28+
* May not be distributed or sublicensed;
29+
* Remain the intellectual property of Stirling PDF and/or its licensors;
30+
* May only be used, copied, or exploited in accordance with the terms of a valid Stirling PDF User License subscription.
31+
32+
Prohibited Actions
33+
34+
Unless explicitly permitted by a paid license or separate agreement, you may not:
35+
36+
* Use the Software in production environments;
37+
* Copy, merge, distribute, sublicense, or sell the Software;
38+
* Remove or alter any licensing or copyright notices;
39+
* Circumvent access restrictions or licensing requirements.
40+
41+
Third-Party Components
42+
43+
The Stirling PDF Software may include components subject to separate open source licenses. Such components remain governed by
44+
their original license terms as provided by their respective owners.
45+
46+
Disclaimer
47+
48+
THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
49+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
50+
LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN
51+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders";
2+
import { type AppProvidersProps } from "@core/components/AppProviders";
3+
import { ChatProvider } from "@app/components/chat/ChatContext";
4+
5+
export type { AppProvidersProps };
6+
7+
export function AppProviders({ children, appConfigRetryOptions, appConfigProviderProps }: AppProvidersProps) {
8+
return (
9+
<ProprietaryAppProviders
10+
appConfigRetryOptions={appConfigRetryOptions}
11+
appConfigProviderProps={appConfigProviderProps}
12+
>
13+
<ChatProvider>
14+
{children}
15+
</ChatProvider>
16+
</ProprietaryAppProviders>
17+
);
18+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { createContext, useContext, useReducer, useCallback, type ReactNode } from "react";
2+
import { useAllFiles } from "@app/contexts/FileContext";
3+
4+
export interface ChatMessage {
5+
id: string;
6+
role: "user" | "assistant";
7+
content: string;
8+
timestamp: number;
9+
}
10+
11+
type AiWorkflowOutcome =
12+
| "answer"
13+
| "not_found"
14+
| "need_content"
15+
| "plan"
16+
| "need_clarification"
17+
| "cannot_do"
18+
| "tool_call"
19+
| "completed"
20+
| "unsupported_capability"
21+
| "cannot_continue";
22+
23+
interface AiWorkflowResponse {
24+
outcome: AiWorkflowOutcome;
25+
answer?: string;
26+
summary?: string;
27+
rationale?: string;
28+
reason?: string;
29+
question?: string;
30+
capability?: string;
31+
message?: string;
32+
evidence?: Array<{ pageNumber: number; text: string }>;
33+
steps?: Array<Record<string, unknown>>;
34+
}
35+
36+
interface ChatState {
37+
messages: ChatMessage[];
38+
isOpen: boolean;
39+
isLoading: boolean;
40+
}
41+
42+
type ChatAction =
43+
| { type: "ADD_MESSAGE"; message: ChatMessage }
44+
| { type: "SET_LOADING"; loading: boolean }
45+
| { type: "TOGGLE_OPEN" }
46+
| { type: "SET_OPEN"; open: boolean };
47+
48+
function chatReducer(state: ChatState, action: ChatAction): ChatState {
49+
switch (action.type) {
50+
case "ADD_MESSAGE":
51+
return { ...state, messages: [...state.messages, action.message] };
52+
case "SET_LOADING":
53+
return { ...state, isLoading: action.loading };
54+
case "TOGGLE_OPEN":
55+
return { ...state, isOpen: !state.isOpen };
56+
case "SET_OPEN":
57+
return { ...state, isOpen: action.open };
58+
}
59+
}
60+
61+
function formatWorkflowResponse(data: AiWorkflowResponse): string {
62+
switch (data.outcome) {
63+
case "answer":
64+
case "completed":
65+
return data.answer ?? data.summary ?? "Done.";
66+
case "need_clarification":
67+
return data.question ?? "Could you clarify your request?";
68+
case "cannot_do":
69+
return data.reason ?? "I'm unable to do that.";
70+
case "not_found":
71+
return data.reason ?? "I couldn't find the requested information.";
72+
case "unsupported_capability":
73+
return data.message ?? `Unsupported capability: ${data.capability ?? "unknown"}`;
74+
case "cannot_continue":
75+
return data.reason ?? "Something went wrong and I can't continue.";
76+
case "plan":
77+
return data.rationale
78+
? `${data.rationale}\n\n${(data.steps ?? []).map((s, i) => `${i + 1}. ${JSON.stringify(s)}`).join("\n")}`
79+
: JSON.stringify(data.steps, null, 2);
80+
case "need_content":
81+
case "tool_call":
82+
return data.rationale ?? data.summary ?? `Processing (${data.outcome})...`;
83+
default:
84+
return data.answer ?? data.summary ?? data.message ?? JSON.stringify(data);
85+
}
86+
}
87+
88+
interface ChatContextValue {
89+
messages: ChatMessage[];
90+
isOpen: boolean;
91+
isLoading: boolean;
92+
toggleOpen: () => void;
93+
setOpen: (open: boolean) => void;
94+
sendMessage: (content: string) => Promise<void>;
95+
}
96+
97+
const ChatContext = createContext<ChatContextValue | null>(null);
98+
99+
const initialState: ChatState = {
100+
messages: [],
101+
isOpen: false,
102+
isLoading: false,
103+
};
104+
105+
export function ChatProvider({ children }: { children: ReactNode }) {
106+
const [state, dispatch] = useReducer(chatReducer, initialState);
107+
const { files: activeFiles } = useAllFiles();
108+
109+
const toggleOpen = useCallback(() => dispatch({ type: "TOGGLE_OPEN" }), []);
110+
const setOpen = useCallback((open: boolean) => dispatch({ type: "SET_OPEN", open }), []);
111+
112+
const sendMessage = useCallback(async (content: string) => {
113+
const userMessage: ChatMessage = {
114+
id: crypto.randomUUID(),
115+
role: "user",
116+
content,
117+
timestamp: Date.now(),
118+
};
119+
dispatch({ type: "ADD_MESSAGE", message: userMessage });
120+
dispatch({ type: "SET_LOADING", loading: true });
121+
122+
try {
123+
const formData = new FormData();
124+
formData.append("userMessage", content);
125+
activeFiles.forEach((file, i) => {
126+
formData.append(`fileInputs[${i}].fileInput`, file);
127+
});
128+
129+
const response = await fetch("/api/v1/ai/orchestrate", {
130+
method: "POST",
131+
body: formData,
132+
});
133+
134+
if (!response.ok) {
135+
throw new Error(`AI engine request failed: ${response.status}`);
136+
}
137+
138+
const data: AiWorkflowResponse = await response.json();
139+
const replyContent = formatWorkflowResponse(data);
140+
const assistantMessage: ChatMessage = {
141+
id: crypto.randomUUID(),
142+
role: "assistant",
143+
content: replyContent,
144+
timestamp: Date.now(),
145+
};
146+
dispatch({ type: "ADD_MESSAGE", message: assistantMessage });
147+
} catch {
148+
const errorMessage: ChatMessage = {
149+
id: crypto.randomUUID(),
150+
role: "assistant",
151+
content: "Failed to get a response. The AI engine may not be available yet.",
152+
timestamp: Date.now(),
153+
};
154+
dispatch({ type: "ADD_MESSAGE", message: errorMessage });
155+
} finally {
156+
dispatch({ type: "SET_LOADING", loading: false });
157+
}
158+
}, [activeFiles]);
159+
160+
return (
161+
<ChatContext.Provider value={{
162+
messages: state.messages,
163+
isOpen: state.isOpen,
164+
isLoading: state.isLoading,
165+
toggleOpen,
166+
setOpen,
167+
sendMessage,
168+
}}>
169+
{children}
170+
</ChatContext.Provider>
171+
);
172+
}
173+
174+
export function useChat(): ChatContextValue {
175+
const context = useContext(ChatContext);
176+
if (!context) {
177+
throw new Error("useChat must be used within a ChatProvider");
178+
}
179+
return context;
180+
}

0 commit comments

Comments
 (0)