Skip to content

Commit 225a5dc

Browse files
Merge pull request #6 from Chintanpatel24/classic-ui-and-demo-mode
Classic Chat UI & Offline Demo Mode
2 parents 40ae8eb + 9296ad2 commit 225a5dc

7 files changed

Lines changed: 289 additions & 62 deletions

File tree

src/config/load.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,11 @@ export function isConfigured(): boolean {
9696
process.env.NIM_API_KEY ||
9797
process.env.OPENAI_API_KEY ||
9898
process.env.ANTHROPIC_API_KEY ||
99-
process.env.ARROWCODE_API_KEY;
99+
process.env.ARROWCODE_API_KEY ||
100+
process.env.DEMO_API_KEY;
100101
if (key) return true;
101102

102-
if (process.env.ARROWCODE_PROVIDER === "ollama") return true;
103+
if (process.env.ARROWCODE_PROVIDER === "ollama" || process.env.ARROWCODE_PROVIDER === "demo" || process.env.ARROWCODE_PROVIDER === "mock") return true;
103104

104105
for (const p of ["ORCH", "FE", "BE", "QA"]) {
105106
if (process.env[`ARROW_${p}_API_KEY`]) return true;
@@ -108,7 +109,7 @@ export function isConfigured(): boolean {
108109
if (existsSync(CONFIG_PATH)) {
109110
try {
110111
const c = parseYaml(readFileSync(CONFIG_PATH, "utf8")) as Partial<ArrowConfig>;
111-
if (c.provider === "ollama" || c.apiKey) return true;
112+
if (c.provider === "ollama" || c.provider === "demo" || c.provider === "mock" || c.apiKey) return true;
112113
} catch {
113114
/* */
114115
}
@@ -122,6 +123,13 @@ function providerDefaults(provider: ProviderId): {
122123
envKey: string;
123124
} {
124125
switch (provider) {
126+
case "demo":
127+
case "mock":
128+
return {
129+
baseUrl: "demo",
130+
model: "demo-v1",
131+
envKey: "DEMO_API_KEY",
132+
};
125133
case "nim":
126134
return {
127135
baseUrl: DEFAULT_NIM_BASE,
@@ -314,7 +322,7 @@ export function loadConfig(overrides: Partial<ArrowConfig> = {}): ArrowConfig {
314322
process.env.OPENAI_API_KEY ||
315323
process.env.ANTHROPIC_API_KEY ||
316324
file.apiKey ||
317-
(provider === "ollama" ? "ollama" : "");
325+
(provider === "ollama" ? "ollama" : (provider === "demo" || provider === "mock" ? "demo" : ""));
318326

319327
const baseUrl =
320328
overrides.baseUrl ||

src/config/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** Shared types for ArrowCode — swarm multi-agent coding harness. */
22

3-
export type ProviderId = "nim" | "openai" | "anthropic" | "ollama" | "groq" | "deepseek" | "gemini" | "openrouter" | "custom";
3+
export type ProviderId = "nim" | "openai" | "anthropic" | "ollama" | "groq" | "deepseek" | "gemini" | "openrouter" | "custom" | "demo" | "mock";
44

55
export type AgentId = "orchestrator" | "frontend" | "backend" | "tester";
66

src/index.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,46 @@ async function main() {
138138
if (!isConfigured()) {
139139
printBanner({ compact: true });
140140
console.log("ArrowCode is not configured yet.\n");
141-
console.log(
142-
"This will create ~/.arrowcode from packaged defaults/ and ask for an API key.\n",
143-
);
141+
console.log("Welcome to ArrowCode! Since you don't have an API key configured yet,");
142+
console.log("you can choose one of the following options to get started immediately:");
143+
console.log(" 1) Use Mock / Demo mode (free, runs locally, no keys needed)");
144+
console.log(" 2) Configure a real LLM provider (NVIDIA NIM, OpenAI, Anthropic, etc.)");
145+
console.log("");
146+
const readline = await import("node:readline");
147+
const rl = readline.createInterface({
148+
input: process.stdin,
149+
output: process.stdout,
150+
});
151+
const choice = await new Promise<string>((res) => {
152+
rl.question("Choose [1-2] (default 1): ", (ans) => res((ans || "").trim()));
153+
});
154+
rl.close();
155+
144156
bootstrapUserHome();
145-
const ok = await runSetupWizard();
146-
if (!ok) process.exit(1);
147-
if (!isConfigured()) {
148-
process.exit(0);
157+
158+
if (choice === "2") {
159+
const ok = await runSetupWizard();
160+
if (!ok) process.exit(1);
161+
if (!isConfigured()) {
162+
process.exit(0);
163+
}
164+
} else {
165+
// Setup demo/mock mode
166+
const { saveEnv, saveConfig } = await import("./config/load");
167+
saveEnv({
168+
ARROWCODE_PROVIDER: "demo",
169+
ARROWCODE_MODEL: "demo-v1",
170+
ARROWCODE_BASE_URL: "demo",
171+
DEMO_API_KEY: "demo",
172+
});
173+
saveConfig({
174+
provider: "demo",
175+
model: "demo-v1",
176+
baseUrl: "demo",
177+
apiKey: "demo",
178+
});
179+
console.log("\nDemo Mode configured! Starting ArrowCode...\n");
180+
await new Promise((resolve) => setTimeout(resolve, 1000));
149181
}
150182
}
151183

src/llm/client.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ export class LLMClient {
3636
this.endpoint = endpoint;
3737
this.model = endpoint.model;
3838
this.provider = endpoint.provider;
39-
if (!endpoint.apiKey && endpoint.provider !== "ollama") {
39+
if (!endpoint.apiKey && endpoint.provider !== "ollama" && endpoint.provider !== "demo" && endpoint.provider !== "mock") {
4040
throw new LLMError(
4141
"No API key for this agent. Set global key or ARROW_<AGENT>_API_KEY / agents.yaml",
4242
);
4343
}
4444
this.client = new OpenAI({
4545
apiKey: endpoint.apiKey || "ollama",
46-
baseURL: endpoint.baseUrl,
46+
baseURL: endpoint.provider === "demo" || endpoint.provider === "mock" ? "http://demo-url" : endpoint.baseUrl,
4747
timeout: 120_000,
4848
maxRetries: 0,
4949
});
@@ -54,6 +54,58 @@ export class LLMClient {
5454
tools?: OpenAITool[],
5555
opts?: { onToken?: (t: string) => void; stream?: boolean },
5656
): Promise<LLMResponse> {
57+
if (this.provider === "demo" || this.provider === "mock") {
58+
// Mock LLM response generator for instant works/tryout!
59+
const userMessage = messages[messages.length - 1]?.content || "";
60+
let replyContent = "Let's explore the workspace and carry out your request.";
61+
const toolCalls: ToolCall[] = [];
62+
63+
// Determine the kind of reply based on the message content
64+
const lower = typeof userMessage === "string" ? userMessage.toLowerCase() : "";
65+
if (lower.includes("[mode=plan]") || lower.includes("plan")) {
66+
replyContent = `\`\`\`arrow-plan
67+
title: Demo Implementation Plan
68+
summary: A mockup flow to demonstrate clean and fast execution.
69+
steps:
70+
1. Explore local directories and files.
71+
2. Setup frontend core elements.
72+
3. Validate using offline tests.
73+
risks:
74+
- Mock mode doesn't execute actual remote files.
75+
acceptance:
76+
- UI works fine.
77+
- Tests pass with green indicators.
78+
agents:
79+
frontend: Core UI setup
80+
backend: API endpoints mocking
81+
tester: Verify everything
82+
\`\`\``;
83+
} else if (lower.includes("[execute]") || lower.includes("execute") || lower.includes("confirm")) {
84+
replyContent = `\`\`\`arrow-ready
85+
Verification: Verified that all mock procedures ran fine and the application is highly optimized.
86+
All tests are passing.
87+
\`\`\``;
88+
} else if (lower.includes("health") || lower.includes("status")) {
89+
replyContent = "Everything is functioning correctly under the demo environment.";
90+
}
91+
92+
if (opts?.onToken) {
93+
// simulate streaming
94+
const parts = replyContent.split(" ");
95+
for (const p of parts) {
96+
opts.onToken(p + " ");
97+
await Bun.sleep(5);
98+
}
99+
}
100+
101+
return {
102+
content: replyContent,
103+
toolCalls,
104+
finishReason: "stop",
105+
usage: { prompt_tokens: messages.length * 10, completion_tokens: replyContent.length / 4 }
106+
};
107+
}
108+
57109
const stream = Boolean(opts?.stream && opts.onToken);
58110
const body: OpenAI.Chat.ChatCompletionCreateParams = {
59111
model: this.model,

0 commit comments

Comments
 (0)