Skip to content

Commit 06447f8

Browse files
committed
refine app studio create setup UX
1 parent 5273ed1 commit 06447f8

3 files changed

Lines changed: 222 additions & 65 deletions

File tree

providers/app-studio/portal/src/App.vue

Lines changed: 74 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
import { api, isProjectAPIInitializingError } from './api'
3333
import {
3434
canSubmitCreatePrompt,
35-
createPromptBlockedMessage,
35+
createSetupItems,
3636
gitConnectionReady,
3737
type ProjectCreateReadiness,
3838
} from './createReadiness'
@@ -368,7 +368,8 @@ const isBuilderVisible = computed(() => !isAppStudioLandingRoute.value || select
368368
const showNewProjectComposer = computed(() => isCreateRoute.value)
369369
const chatPaneStyle = computed(() => ({ flexBasis: `${splitWidth.value}%` }))
370370
const assistantResumeBusy = computed(() => Object.keys(permissionBusy.value).length > 0 || Object.keys(followUpBusy.value).length > 0)
371-
const canStartProjectFromPrompt = computed(() => canSubmitCreatePrompt(prompt.value, createReadiness.value))
371+
const llmConfigured = computed(() => llmSettings.value?.configured ?? false)
372+
const canStartProjectFromPrompt = computed(() => canSubmitCreatePrompt(prompt.value, createReadiness.value) && llmConfigured.value)
372373
const canSendPrompt = computed(() => (llmSettings.value?.configured ?? false) && prompt.value.trim().length > 0 && !messageStreaming.value && !assistantResumeBusy.value)
373374
const settingsProject = computed(() => (isAppStudioLandingRoute.value ? null : selected.value))
374375
const settingsTitle = computed(() => (settingsProject.value ? 'Project settings' : 'LLM settings'))
@@ -386,26 +387,17 @@ const conversationWorkingLabel = computed(() => {
386387
})
387388
const gitConnectionCreateReady = computed(() => gitConnectionReady(createReadiness.value))
388389
const createReadinessChecking = computed(() => createReadinessLoading.value || (!!props.ctx?.token && createReadiness.value === null && !createReadinessError.value))
389-
const createReadinessBlockMessage = computed(() => {
390-
if (createReadinessError.value) return createReadinessError.value
391-
if (createReadinessChecking.value) return 'Checking Git connection...'
392-
return createPromptBlockedMessage(createReadiness.value)
393-
})
390+
const createSetupItemsForPrompt = computed(() => createSetupItems({
391+
readiness: createReadiness.value,
392+
llmConfigured: llmConfigured.value,
393+
checkingGit: createReadinessChecking.value,
394+
}))
394395
const createPromptSubmitTitle = computed(() => {
395-
if (!gitConnectionCreateReady.value) return 'Connect Git before creating a durable project'
396-
return llmSettings.value?.configured ? 'Create project and send prompt' : 'Configure LLM settings before creating a project'
397-
})
398-
const createPromptSubmitLabel = computed(() => {
399-
if (!gitConnectionCreateReady.value) return createReadinessChecking.value ? 'Checking Git' : 'Connect Git'
400-
return llmSettings.value?.configured ? 'Create and send' : 'Set up and send'
401-
})
402-
const llmConfigured = computed(() => llmSettings.value?.configured ?? false)
403-
const workspaceSetupReady = computed(() => gitConnectionCreateReady.value && llmConfigured.value)
404-
const workspaceSetupLabel = computed(() => {
405-
if (!gitConnectionCreateReady.value) return createReadinessChecking.value ? 'Checking Git' : 'Git setup needed'
406-
if (!llmConfigured.value) return 'LLM setup needed'
407-
return 'Workspace ready'
396+
if (createSetupItemsForPrompt.value.length > 0) return 'Complete setup before creating a project'
397+
return prompt.value.trim() ? 'Create project and send prompt' : 'Describe what you want to build'
408398
})
399+
const createSetupVisible = computed(() => createSetupItemsForPrompt.value.length > 0 || !!createReadinessError.value)
400+
const createSetupErrorMessage = computed(() => createReadinessError.value || '')
409401
const deleteProjectMessage = computed(() => {
410402
const project = deleteProjectTarget.value
411403
if (!project) return ''
@@ -1260,20 +1252,16 @@ async function clearLLMKey() {
12601252
async function createProjectFromPrompt() {
12611253
const content = prompt.value.trim()
12621254
if (!content) return
1263-
if (!await ensureGitConnectionReady()) return
1264-
if (!llmSettings.value?.configured) {
1265-
openSettings()
1266-
return
1267-
}
1255+
if (!await ensureCreateSetupReady()) return
12681256
await createProjectAndStartConversation(content)
12691257
}
12701258
1271-
async function ensureGitConnectionReady(): Promise<boolean> {
1259+
async function ensureCreateSetupReady(): Promise<boolean> {
12721260
if (!gitConnectionCreateReady.value && !createReadinessLoading.value) {
12731261
await loadCreateReadiness()
12741262
}
1275-
if (gitConnectionCreateReady.value) return true
1276-
error.value = createReadinessError.value || createPromptBlockedMessage(createReadiness.value)
1263+
if (gitConnectionCreateReady.value && llmConfigured.value) return true
1264+
error.value = null
12771265
return false
12781266
}
12791267
@@ -2811,6 +2799,7 @@ function repositoryCommitFilesLabel(commit: ProjectRepositoryCommit): string {
28112799
Back to projects
28122800
</button>
28132801
<button
2802+
v-if="!showNewProjectComposer"
28142803
type="button"
28152804
class="flex h-9 items-center gap-2 rounded-md border border-border-subtle bg-surface-raised px-3 text-[13px] font-medium text-text-secondary transition hover:bg-surface-hover hover:text-text-primary"
28162805
title="LLM settings"
@@ -2940,17 +2929,7 @@ function repositoryCommitFilesLabel(commit: ProjectRepositoryCommit): string {
29402929
<main class="flex min-h-0 flex-1 items-center justify-center py-4">
29412930
<section class="w-full max-w-[1060px]">
29422931
<div class="mx-auto flex max-w-[760px] flex-col items-center text-center">
2943-
<span
2944-
class="inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-[12px] font-medium"
2945-
:class="workspaceSetupReady
2946-
? 'border-success/30 bg-success-subtle text-success'
2947-
: 'border-warning/30 bg-warning-subtle text-warning'"
2948-
>
2949-
<Check v-if="workspaceSetupReady" class="h-3.5 w-3.5" :stroke-width="2" />
2950-
<Settings2 v-else class="h-3.5 w-3.5" :stroke-width="1.75" />
2951-
{{ workspaceSetupLabel }}
2952-
</span>
2953-
<h2 class="mt-5 text-[44px] font-semibold leading-[1.05] text-text-primary md:text-[56px]">
2932+
<h2 class="text-[44px] font-semibold leading-[1.05] text-text-primary md:text-[56px]">
29542933
What do you want to build?
29552934
</h2>
29562935
<p class="mt-4 max-w-[62ch] text-[14px] leading-6 text-text-muted">
@@ -2985,44 +2964,74 @@ function repositoryCommitFilesLabel(commit: ProjectRepositoryCommit): string {
29852964
<X class="h-3.5 w-3.5" :stroke-width="2" />
29862965
</button>
29872966
</span>
2988-
<button
2989-
v-if="!llmSettings?.configured"
2990-
type="button"
2991-
class="inline-flex items-center gap-1.5 rounded-md border border-accent/30 bg-accent/10 px-2.5 py-1.5 text-[12px] font-medium text-accent transition hover:bg-accent/20"
2992-
@click="openSettings"
2993-
>
2994-
<Settings2 class="h-3.5 w-3.5" :stroke-width="1.75" />
2995-
Set up LLM
2996-
</button>
2997-
<span v-else class="text-[12px] text-text-muted">
2967+
<span v-if="!createSetupVisible" class="text-[12px] text-text-muted">
29982968
The first message will create the project and start the conversation.
29992969
</span>
3000-
<span
3001-
v-if="createReadinessBlockMessage"
3002-
class="inline-flex items-center gap-1.5 rounded-md border border-warning/30 bg-warning-subtle px-2.5 py-1.5 text-[12px] font-medium text-warning"
3003-
>
3004-
<GitBranch class="h-3.5 w-3.5" :stroke-width="1.75" />
3005-
<template v-if="isMissingCodeConnectionError(createReadinessBlockMessage)">
3006-
<a :href="CODE_CONNECTIONS_URL" class="underline underline-offset-2 hover:text-warning/80">
3007-
Connect Git
3008-
</a>
3009-
<span>to create a durable project.</span>
3010-
</template>
3011-
<template v-else>{{ createReadinessBlockMessage }}</template>
3012-
</span>
30132970
</div>
30142971
<button
30152972
class="inline-flex h-9 items-center justify-center gap-2 rounded-md border border-accent/30 bg-accent/10 px-3 text-[13px] font-medium text-accent transition hover:bg-accent/20 disabled:cursor-not-allowed disabled:opacity-60"
30162973
type="submit"
30172974
:disabled="busy || !canStartProjectFromPrompt"
30182975
:title="createPromptSubmitTitle"
30192976
>
3020-
<Plus v-if="gitConnectionCreateReady && llmSettings?.configured" class="h-4 w-4" :stroke-width="2" />
3021-
<Settings2 v-else class="h-4 w-4" :stroke-width="1.75" />
3022-
{{ createPromptSubmitLabel }}
2977+
<Plus class="h-4 w-4" :stroke-width="2" />
2978+
Create and send
30232979
</button>
30242980
</div>
30252981
</div>
2982+
<div
2983+
v-if="createSetupVisible"
2984+
class="mt-3 rounded-lg border border-border-subtle bg-surface-raised/70 p-3 text-left"
2985+
>
2986+
<div class="mb-2 flex items-center gap-2 text-[12px] font-semibold text-text-primary">
2987+
<Settings2 class="h-3.5 w-3.5 text-accent" :stroke-width="1.75" />
2988+
Complete setup before creating
2989+
</div>
2990+
<div v-if="createSetupErrorMessage" class="mb-2 text-[12px] text-danger">{{ createSetupErrorMessage }}</div>
2991+
<div class="grid gap-2">
2992+
<div
2993+
v-for="item in createSetupItemsForPrompt"
2994+
:key="item.id"
2995+
class="flex min-h-10 flex-wrap items-center justify-between gap-2 rounded-md border border-border-subtle bg-surface px-3 py-2"
2996+
>
2997+
<div class="flex min-w-0 items-center gap-2">
2998+
<span
2999+
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border"
3000+
:class="item.status === 'ready'
3001+
? 'border-success/30 bg-success-subtle text-success'
3002+
: item.status === 'checking'
3003+
? 'border-warning/30 bg-warning-subtle text-warning'
3004+
: 'border-border-subtle bg-surface-raised text-text-muted'"
3005+
>
3006+
<Check v-if="item.status === 'ready'" class="h-3.5 w-3.5" :stroke-width="2" />
3007+
<Loader2 v-else-if="item.status === 'checking'" class="h-3.5 w-3.5 animate-spin" :stroke-width="1.75" />
3008+
<GitBranch v-else-if="item.id === 'git'" class="h-3.5 w-3.5" :stroke-width="1.75" />
3009+
<Settings2 v-else class="h-3.5 w-3.5" :stroke-width="1.75" />
3010+
</span>
3011+
<span class="truncate text-[13px] font-medium text-text-primary">{{ item.label }}</span>
3012+
</div>
3013+
<span v-if="item.status === 'ready'" class="text-[12px] font-medium text-success">Ready</span>
3014+
<span v-else-if="item.status === 'checking'" class="text-[12px] font-medium text-warning">Checking</span>
3015+
<a
3016+
v-else-if="item.action === 'connect-git'"
3017+
:href="CODE_CONNECTIONS_URL"
3018+
class="inline-flex h-8 items-center gap-1.5 rounded-md border border-accent/30 bg-accent/10 px-2.5 text-[12px] font-medium text-accent transition hover:bg-accent/20"
3019+
>
3020+
<GitBranch class="h-3.5 w-3.5" :stroke-width="1.75" />
3021+
{{ item.actionLabel }}
3022+
</a>
3023+
<button
3024+
v-else-if="item.action === 'setup-llm'"
3025+
type="button"
3026+
class="inline-flex h-8 items-center gap-1.5 rounded-md border border-accent/30 bg-accent/10 px-2.5 text-[12px] font-medium text-accent transition hover:bg-accent/20"
3027+
@click="openSettings"
3028+
>
3029+
<Settings2 class="h-3.5 w-3.5" :stroke-width="1.75" />
3030+
{{ item.actionLabel }}
3031+
</button>
3032+
</div>
3033+
</div>
3034+
</div>
30263035
</form>
30273036

30283037
<div class="mt-6 grid gap-2 sm:grid-cols-2 xl:grid-cols-5">

providers/app-studio/portal/src/createReadiness.test.mjs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import test from 'node:test'
55
import ts from 'typescript'
66

77
const source = await readFile(new URL('./createReadiness.ts', import.meta.url), 'utf8')
8+
const appSource = await readFile(new URL('./App.vue', import.meta.url), 'utf8')
89
const { outputText } = ts.transpileModule(source, {
910
compilerOptions: {
1011
module: ts.ModuleKind.ES2022,
@@ -14,6 +15,7 @@ const { outputText } = ts.transpileModule(source, {
1415
const moduleURL = `data:text/javascript;base64,${Buffer.from(outputText).toString('base64')}`
1516
const {
1617
canSubmitCreatePrompt,
18+
createSetupItems,
1719
createPromptBlockedMessage,
1820
} = await import(moduleURL)
1921

@@ -54,3 +56,86 @@ test('still requires the user to type a prompt before submitting', () => {
5456

5557
assert.equal(canSubmitCreatePrompt(' ', readiness), false)
5658
})
59+
60+
test('summarizes Git and LLM setup as one checklist', () => {
61+
const items = createSetupItems({
62+
readiness: {
63+
gitConnection: {
64+
ready: false,
65+
message: 'You need to connect to a Git account before you can continue',
66+
},
67+
},
68+
llmConfigured: false,
69+
checkingGit: false,
70+
})
71+
72+
assert.deepEqual(items, [
73+
{
74+
id: 'git',
75+
label: 'Git connection',
76+
status: 'missing',
77+
actionLabel: 'Connect Git',
78+
action: 'connect-git',
79+
},
80+
{
81+
id: 'llm',
82+
label: 'LLM credentials',
83+
status: 'missing',
84+
actionLabel: 'Set up LLM',
85+
action: 'setup-llm',
86+
},
87+
])
88+
})
89+
90+
test('collapses the setup checklist once all prerequisites are ready', () => {
91+
const items = createSetupItems({
92+
readiness: {
93+
gitConnection: {
94+
ready: true,
95+
connectionRef: 'github',
96+
},
97+
},
98+
llmConfigured: true,
99+
checkingGit: false,
100+
})
101+
102+
assert.deepEqual(items, [])
103+
})
104+
105+
test('keeps ready setup rows visible while another prerequisite is missing', () => {
106+
const items = createSetupItems({
107+
readiness: {
108+
gitConnection: {
109+
ready: true,
110+
connectionRef: 'github',
111+
},
112+
},
113+
llmConfigured: false,
114+
checkingGit: false,
115+
})
116+
117+
assert.deepEqual(items, [
118+
{
119+
id: 'git',
120+
label: 'Git connection',
121+
status: 'ready',
122+
},
123+
{
124+
id: 'llm',
125+
label: 'LLM credentials',
126+
status: 'missing',
127+
actionLabel: 'Set up LLM',
128+
action: 'setup-llm',
129+
},
130+
])
131+
})
132+
133+
test('new-project route has one setup surface and a stable create button label', () => {
134+
assert.equal(appSource.includes('workspaceSetupLabel'), false)
135+
assert.equal(appSource.includes('createPromptSubmitLabel'), false)
136+
assert.equal(appSource.includes('to create a durable project.'), false)
137+
assert.match(appSource, /<button\s+v-if="!showNewProjectComposer"[\s\S]*title="LLM settings"/)
138+
assert.equal(appSource.includes('error.value = gitConnectionCreateReady.value ? null : createReadinessError.value || createPromptBlockedMessage(createReadiness.value)'), false)
139+
assert.match(appSource, /if \(gitConnectionCreateReady\.value && llmConfigured\.value\) return true\s+error\.value = null\s+return false/)
140+
assert.match(appSource, />\s*Create and send\s*</)
141+
})

providers/app-studio/portal/src/createReadiness.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ export interface ProjectCreateReadiness {
66
}
77
}
88

9+
export interface CreateSetupItemsInput {
10+
readiness: ProjectCreateReadiness | null
11+
llmConfigured: boolean
12+
checkingGit: boolean
13+
}
14+
15+
export interface CreateSetupItem {
16+
id: 'git' | 'llm'
17+
label: string
18+
status: 'checking' | 'ready' | 'missing'
19+
actionLabel?: string
20+
action?: 'connect-git' | 'setup-llm'
21+
}
22+
923
const defaultGitConnectionMessage = 'You need to connect to a Git account before you can continue'
1024

1125
export function gitConnectionReady(readiness: ProjectCreateReadiness | null): boolean {
@@ -20,3 +34,52 @@ export function createPromptBlockedMessage(readiness: ProjectCreateReadiness | n
2034
export function canSubmitCreatePrompt(prompt: string, readiness: ProjectCreateReadiness | null): boolean {
2135
return prompt.trim().length > 0 && gitConnectionReady(readiness)
2236
}
37+
38+
export function createSetupItems(input: CreateSetupItemsInput): CreateSetupItem[] {
39+
const gitReady = gitConnectionReady(input.readiness)
40+
if (gitReady && input.llmConfigured) return []
41+
42+
const items: CreateSetupItem[] = [gitSetupItem(gitReady, input.checkingGit)]
43+
44+
items.push(
45+
input.llmConfigured
46+
? {
47+
id: 'llm',
48+
label: 'LLM credentials',
49+
status: 'ready',
50+
}
51+
: {
52+
id: 'llm',
53+
label: 'LLM credentials',
54+
status: 'missing',
55+
actionLabel: 'Set up LLM',
56+
action: 'setup-llm',
57+
},
58+
)
59+
60+
return items
61+
}
62+
63+
function gitSetupItem(gitReady: boolean, checkingGit: boolean): CreateSetupItem {
64+
if (checkingGit) {
65+
return {
66+
id: 'git',
67+
label: 'Git connection',
68+
status: 'checking',
69+
}
70+
}
71+
if (gitReady) {
72+
return {
73+
id: 'git',
74+
label: 'Git connection',
75+
status: 'ready',
76+
}
77+
}
78+
return {
79+
id: 'git',
80+
label: 'Git connection',
81+
status: 'missing',
82+
actionLabel: 'Connect Git',
83+
action: 'connect-git',
84+
}
85+
}

0 commit comments

Comments
 (0)