feat: v1.3.0 — plan-first workflow, OpenRouter provider, enhanced prompt engine
Major changes: - Plan-first workflow: AI generates structured plan before code, with plan review card (Modify Plan / Start Coding / Skip to Code) - Post-coding UX: Preview + Request Modifications buttons after code gen - OpenRouter integration: 4th AI provider with 20+ model support - Enhanced prompt engine: 9 strategies, 11+ intent patterns, modular - PLAN MODE system prompt block in all 4 services - Fixed stale React closure in approveAndGenerate with isApproval flag - Fixed canvas auto-opening during plan phase with wasIdle gate - Updated README, CHANGELOG, .env.example, version bump to 1.3.0
This commit is contained in:
@@ -54,6 +54,10 @@ export default function SettingsPanel() {
|
||||
setApiKey("zai", keys.zai);
|
||||
modelAdapter.updateZaiApiKey(keys.zai);
|
||||
}
|
||||
if (keys.openrouter) {
|
||||
setApiKey("openrouter", keys.openrouter);
|
||||
modelAdapter.updateOpenRouterApiKey(keys.openrouter);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load API keys:", e);
|
||||
}
|
||||
@@ -84,7 +88,7 @@ export default function SettingsPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
const validateApiKey = async (provider: "qwen" | "ollama" | "zai") => {
|
||||
const validateApiKey = async (provider: "qwen" | "ollama" | "zai" | "openrouter") => {
|
||||
const key = apiKeys[provider];
|
||||
if (!key || key.trim().length === 0) {
|
||||
setApiValidationStatus(provider, { valid: false, error: "API key is required" });
|
||||
@@ -149,6 +153,9 @@ export default function SettingsPanel() {
|
||||
case "zai":
|
||||
modelAdapter.updateZaiApiKey(value);
|
||||
break;
|
||||
case "openrouter":
|
||||
modelAdapter.updateOpenRouterApiKey(value);
|
||||
break;
|
||||
}
|
||||
|
||||
// Debounce validation (500ms)
|
||||
@@ -187,7 +194,7 @@ export default function SettingsPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIndicator = (provider: "qwen" | "ollama" | "zai") => {
|
||||
const getStatusIndicator = (provider: "qwen" | "ollama" | "zai" | "openrouter") => {
|
||||
const status = apiValidationStatus[provider];
|
||||
|
||||
if (validating[provider]) {
|
||||
@@ -439,6 +446,63 @@ export default function SettingsPanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-start">
|
||||
<label className="flex items-center gap-2 text-xs lg:text-sm font-medium">
|
||||
<Server className="h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
OpenRouter API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showApiKey.openrouter ? "text" : "password"}
|
||||
placeholder={t.enterKey("OpenRouter")}
|
||||
value={apiKeys.openrouter || ""}
|
||||
onChange={(e) => handleApiKeyChange("openrouter", e.target.value)}
|
||||
className="font-mono text-xs lg:text-sm pr-24"
|
||||
/>
|
||||
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||||
{getStatusIndicator("openrouter")}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => setShowApiKey((prev) => ({ ...prev, openrouter: !prev.openrouter }))}
|
||||
>
|
||||
{showApiKey.openrouter ? (
|
||||
<EyeOff className="h-3 w-3" />
|
||||
) : (
|
||||
<Eye className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<p className="text-[10px] lg:text-xs text-muted-foreground">
|
||||
{t.getApiKey}{" "}
|
||||
<a
|
||||
href="https://openrouter.ai/keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
openrouter.ai/keys
|
||||
</a>
|
||||
</p>
|
||||
{apiKeys.openrouter && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-[9px] lg:text-[10px] w-fit"
|
||||
onClick={() => validateApiKey("openrouter")}
|
||||
disabled={validating.openrouter}
|
||||
>
|
||||
<RefreshCw className={`h-2.5 w-2.5 mr-1 ${validating.openrouter ? "animate-spin" : ""}`} />
|
||||
Test
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleSave} className="w-full h-9 lg:h-10 text-xs lg:text-sm">
|
||||
<Save className="mr-1.5 lg:mr-2 h-3.5 w-3.5 lg:h-4 lg:w-4" />
|
||||
{t.saveKeys}
|
||||
@@ -455,7 +519,7 @@ export default function SettingsPanel() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 lg:space-y-4 p-4 lg:p-6 pt-0 lg:pt-0">
|
||||
<div className="grid gap-2 lg:gap-3">
|
||||
{(["qwen", "ollama", "zai"] as const).map((provider) => (
|
||||
{(["qwen", "ollama", "zai", "openrouter"] as const).map((provider) => (
|
||||
<button
|
||||
key={provider}
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
@@ -468,11 +532,12 @@ export default function SettingsPanel() {
|
||||
<Server className="h-4 w-4 lg:h-5 lg:w-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium capitalize text-sm lg:text-base">{provider}</h3>
|
||||
<h3 className="font-medium capitalize text-sm lg:text-base">{provider === "openrouter" ? "OpenRouter" : provider}</h3>
|
||||
<p className="text-[10px] lg:text-sm text-muted-foreground truncate">
|
||||
{provider === "qwen" && t.qwenDesc}
|
||||
{provider === "ollama" && t.ollamaDesc}
|
||||
{provider === "zai" && t.zaiDesc}
|
||||
{provider === "openrouter" && t.openrouterDesc}
|
||||
</p>
|
||||
</div>
|
||||
{selectedProvider === provider && (
|
||||
|
||||
Reference in New Issue
Block a user