Skip to content

Commit 8c92e0f

Browse files
authored
Merge pull request #917 from Kiln-AI/sfierro/kiln-copilot-upsell
Add Kiln Copilot options modal on spec creation
2 parents 60828b6 + 0af44e3 commit 8c92e0f

File tree

14 files changed

+796
-527
lines changed

14 files changed

+796
-527
lines changed

app/desktop/studio_server/provider_api.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,11 +1140,17 @@ async def connect_bedrock(key_data: dict):
11401140

11411141
async def connect_kiln_copilot(key: str):
11421142
base_url = os.environ.get("KILN_SERVER_BASE_URL", "https://api.kiln.tech")
1143-
async with httpx.AsyncClient() as client:
1144-
response = await client.get(
1145-
f"{base_url}/v1/verify_api_key",
1146-
headers={"Authorization": f"Bearer {key}"},
1147-
timeout=20,
1143+
try:
1144+
async with httpx.AsyncClient() as client:
1145+
response = await client.get(
1146+
f"{base_url}/v1/verify_api_key",
1147+
headers={"Authorization": f"Bearer {key}"},
1148+
timeout=20,
1149+
)
1150+
except httpx.RequestError as e:
1151+
return JSONResponse(
1152+
status_code=400,
1153+
content={"message": f"Failed to connect to Kiln Copilot. Error: {e!s}"},
11481154
)
11491155

11501156
if response.status_code == 200:

app/desktop/studio_server/test_provider_api.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,35 @@ def test_connect_api_key_kiln_copilot_empty_key(client):
165165
assert response.json() == {"detail": "API Key not found"}
166166

167167

168+
@patch("app.desktop.studio_server.provider_api.httpx.AsyncClient.get")
169+
def test_connect_api_key_kiln_copilot_failure(mock_httpx_get, client):
170+
mock_response = MagicMock()
171+
mock_response.status_code = 401
172+
mock_response.json.return_value = {"message": "Invalid API key"}
173+
mock_httpx_get.return_value = mock_response
174+
175+
response = client.post(
176+
"/api/provider/connect_api_key",
177+
json={"provider": "kiln_copilot", "key_data": {"API Key": "invalid_key"}},
178+
)
179+
180+
assert response.status_code == 401
181+
assert response.json() == {"message": "Invalid API key"}
182+
183+
184+
@patch("app.desktop.studio_server.provider_api.httpx.AsyncClient.get")
185+
def test_connect_api_key_kiln_copilot_network_error(mock_httpx_get, client):
186+
mock_httpx_get.side_effect = httpx.RequestError("Network error")
187+
188+
response = client.post(
189+
"/api/provider/connect_api_key",
190+
json={"provider": "kiln_copilot", "key_data": {"API Key": "test_key"}},
191+
)
192+
193+
assert response.status_code == 400
194+
assert "Failed to connect" in response.json()["message"]
195+
196+
168197
@patch("app.desktop.studio_server.provider_api.requests.get")
169198
@patch("app.desktop.studio_server.provider_api.Config.shared")
170199
def test_connect_openai_success(mock_config_shared, mock_requests_get, client):

app/web_ui/src/lib/ui/dialog.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
disabled?: boolean
2424
loading?: boolean
2525
hide?: boolean
26+
width?: "normal" | "wide"
2627
}
2728
export let action_buttons: ActionButton[] = []
2829
let action_running = false
@@ -187,9 +188,9 @@
187188
</form>
188189
{:else}
189190
<button
190-
class="btn btn-sm h-10 min-w-24 {button.isPrimary
191-
? 'btn-primary'
192-
: ''}
191+
class="btn btn-sm h-10 min-w-24 {button.width === 'wide'
192+
? 'w-full'
193+
: ''} {button.isPrimary ? 'btn-primary' : ''}
193194
{button.isError ? 'btn-error' : ''}
194195
{button.isWarning ? 'btn-warning' : ''}"
195196
disabled={button.disabled || button.loading}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<script lang="ts">
2+
import { onMount } from "svelte"
3+
import createKindeClient from "@kinde-oss/kinde-auth-pkce-js"
4+
import { base_url } from "$lib/api_client"
5+
import posthog from "posthog-js"
6+
import { env } from "$env/dynamic/public"
7+
8+
export let onSuccess: () => void
9+
export let showTitle = true
10+
export let showCheckmark = false
11+
12+
let kindeClient: Awaited<ReturnType<typeof createKindeClient>> | null = null
13+
let apiKey = ""
14+
let apiKeyError = false
15+
let apiKeyMessage: string | null = null
16+
let submitting = false
17+
18+
const KINDE_ACCOUNT_DOMAIN =
19+
env.PUBLIC_KINDE_ACCOUNT_DOMAIN || "https://account.kiln.tech"
20+
const KINDE_ACCOUNT_CLIENT_ID =
21+
env.PUBLIC_KINDE_ACCOUNT_CLIENT_ID || "2428f47a1e0b404b82e68400a2d580c6"
22+
23+
async function initKindeClient() {
24+
if (kindeClient) return kindeClient
25+
26+
kindeClient = await createKindeClient({
27+
client_id: KINDE_ACCOUNT_CLIENT_ID,
28+
domain: KINDE_ACCOUNT_DOMAIN,
29+
redirect_uri: window.location.origin + window.location.pathname,
30+
on_redirect_callback: () => {},
31+
})
32+
33+
return kindeClient
34+
}
35+
36+
async function openSignup() {
37+
try {
38+
const kinde = await initKindeClient()
39+
if (!kinde) {
40+
apiKeyError = true
41+
apiKeyMessage = "Kinde configuration is missing"
42+
return
43+
}
44+
45+
await kinde.register()
46+
} catch (e) {
47+
console.error("openSignup error", e)
48+
apiKeyError = true
49+
apiKeyMessage = "Failed to open Kiln Copilot signup"
50+
}
51+
}
52+
53+
async function openSelfServePortal() {
54+
try {
55+
const kinde = await initKindeClient()
56+
if (!kinde) {
57+
apiKeyError = true
58+
apiKeyMessage = "Please sign up first"
59+
return
60+
}
61+
62+
const accessToken = await kinde.getToken()
63+
if (!accessToken) {
64+
apiKeyError = true
65+
apiKeyMessage = "Please sign up first before accessing the portal"
66+
return
67+
}
68+
69+
const response = await fetch(
70+
`${KINDE_ACCOUNT_DOMAIN}/account_api/v1/portal_link`,
71+
{
72+
headers: {
73+
Authorization: `Bearer ${accessToken}`,
74+
},
75+
},
76+
)
77+
78+
if (!response.ok) {
79+
throw new Error("Failed to generate portal link")
80+
}
81+
82+
const data = await response.json()
83+
window.open(data.url, "_blank")
84+
} catch (e) {
85+
console.error("openSelfServePortal error", e)
86+
apiKeyError = true
87+
apiKeyMessage =
88+
"Failed to open self-serve portal. Please try signing up first."
89+
}
90+
}
91+
92+
export async function submitApiKey() {
93+
if (!apiKey.trim()) {
94+
apiKeyError = true
95+
return false
96+
}
97+
98+
apiKeyError = false
99+
apiKeyMessage = null
100+
submitting = true
101+
102+
try {
103+
const res = await fetch(base_url + "/api/provider/connect_api_key", {
104+
method: "POST",
105+
headers: {
106+
"Content-Type": "application/json",
107+
},
108+
body: JSON.stringify({
109+
provider: "kiln_copilot",
110+
key_data: {
111+
"API Key": apiKey,
112+
},
113+
}),
114+
})
115+
116+
const data = await res.json()
117+
118+
if (!res.ok) {
119+
apiKeyMessage =
120+
data.message || data.detail || "Failed to connect to provider"
121+
apiKeyError = true
122+
return false
123+
}
124+
125+
posthog.capture("connect_provider", {
126+
provider_id: "kiln_copilot",
127+
})
128+
129+
onSuccess()
130+
return true
131+
} catch (e) {
132+
console.error("submitApiKey error", e)
133+
apiKeyMessage = "Failed to connect to provider (Exception: " + e + ")"
134+
apiKeyError = true
135+
return false
136+
} finally {
137+
submitting = false
138+
}
139+
}
140+
141+
onMount(async () => {
142+
if (
143+
window.location.search.includes("code=") ||
144+
window.location.search.includes("state=")
145+
) {
146+
await initKindeClient()
147+
}
148+
})
149+
</script>
150+
151+
{#if showTitle}
152+
<h1 class="text-xl font-medium text-center mb-2">Connect Kiln Copilot</h1>
153+
{/if}
154+
155+
<ol class="mb-2 text-gray-700">
156+
<li class="list-decimal pl-1 mx-8 mb-4">
157+
<button class="link" on:click={openSignup}>Sign Up</button>
158+
to create your Kiln Copilot account.
159+
</li>
160+
<li class="list-decimal pl-1 mx-8 my-4">
161+
After registration,
162+
<button class="link" on:click={openSelfServePortal}>
163+
open the self-serve portal.
164+
</button>
165+
Go to 'API keys' section and create an API key.
166+
</li>
167+
<li class="list-decimal pl-1 mx-8 my-4">
168+
Copy your API key, paste it below and click 'Connect'.
169+
</li>
170+
</ol>
171+
172+
{#if apiKeyMessage}
173+
<p class="text-error text-center pb-4">{apiKeyMessage}</p>
174+
{/if}
175+
176+
<div class="flex flex-row gap-4 items-center">
177+
<div class="grow flex flex-col gap-2">
178+
<input
179+
type="text"
180+
id="API Key"
181+
placeholder="API Key"
182+
class="input input-bordered w-full max-w-[300px] {apiKeyError
183+
? 'input-error'
184+
: ''}"
185+
bind:value={apiKey}
186+
on:input={() => (apiKeyError = false)}
187+
/>
188+
</div>
189+
<button
190+
class="btn min-w-[130px]"
191+
on:click={submitApiKey}
192+
disabled={submitting}
193+
>
194+
{#if submitting}
195+
<div class="loading loading-spinner loading-md"></div>
196+
{:else if showCheckmark}
197+
<img
198+
src="/images/circle-check.svg"
199+
class="size-6 group-hover:hidden"
200+
alt="Connected"
201+
/>
202+
{:else}
203+
Connect
204+
{/if}
205+
</button>
206+
</div>

app/web_ui/src/routes/(app)/settings/providers/kiln_copilot/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</script>
66

77
<AppPage
8-
title="Connect Kiln Copilot"
8+
title="Kiln Copilot"
99
breadcrumbs={[
1010
{ label: "Settings", href: "/settings" },
1111
{ label: "AI Providers", href: "/settings/providers" },

app/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/[spec_id]/[eval_id]/compare_run_configs/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@
145145
try {
146146
task = await load_task(project_id, task_id)
147147
if (!task) {
148-
throw createKilnError("Task not found")
148+
throw new Error("Task not found")
149149
}
150150
} catch (err) {
151151
task_error = createKilnError(err)

app/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/compare/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@
176176
try {
177177
task = await load_task(project_id, task_id)
178178
if (!task) {
179-
throw createKilnError("Task not found")
179+
throw new Error("Task not found")
180180
}
181181
} catch (err) {
182182
task_error = createKilnError(err)

0 commit comments

Comments
 (0)