Skip to content

Commit 49d59ed

Browse files
authored
Refactor AI model configuration into shared YAML catalog for backend/frontend consistency (#23)
1 parent 3bb9062 commit 49d59ed

File tree

24 files changed

+701
-203
lines changed

24 files changed

+701
-203
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ jobs:
180180
name: Docker Build & Push
181181
runs-on: ubuntu-latest
182182
needs: [quality, test]
183+
if: github.event_name != 'pull_request'
183184
permissions:
184185
contents: read
185186
packages: write

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ temp/
7777

7878
# AI model files and caches
7979
models/
80+
!pkg/ai/models/
81+
!pkg/ai/models/*.yaml
82+
!pkg/ai/models/*.go
8083
*.model
8184
*.cache
8285
*.pkl

Dockerfile

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,49 @@
11
# Build stage
2-
FROM golang:1.24-alpine AS builder
2+
# syntax=docker/dockerfile:1.7
3+
ARG BUILDPLATFORM
4+
ARG TARGETPLATFORM
5+
6+
FROM --platform=$BUILDPLATFORM node:20-alpine AS frontend-builder
7+
WORKDIR /workspace
8+
9+
# Copy repository contents (needed because frontend build emits to ../pkg/plugin/assets)
10+
COPY . .
11+
12+
# Cache npm modules to speed up rebuilds
13+
RUN --mount=type=cache,target=/root/.npm npm --prefix frontend ci
14+
RUN npm --prefix frontend run build
15+
16+
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
17+
ARG TARGETOS
18+
ARG TARGETARCH
319

420
# Install build dependencies
5-
RUN apk add --no-cache git ca-certificates tzdata nodejs npm
21+
RUN apk add --no-cache git ca-certificates tzdata
22+
23+
# Enable module caching
24+
ENV GOMODCACHE=/go/pkg/mod
25+
ENV GOCACHE=/root/.cache/go-build
626

727
# Set working directory
8-
WORKDIR /app
28+
WORKDIR /workspace
929

10-
# Copy all source code first (needed for go.mod replace directive)
11-
COPY . .
30+
# Copy go module files and download dependencies
31+
COPY go.mod go.sum ./
32+
RUN --mount=type=cache,target=/root/.cache/go-build \
33+
--mount=type=cache,target=/go/pkg/mod \
34+
go mod download
1235

13-
# Download dependencies (replace directive requires source to be present)
14-
RUN go mod download
36+
# Copy source code
37+
COPY . .
1538

16-
# Build frontend assets required for Go embed
17-
RUN npm ci --prefix frontend
18-
RUN npm run build --prefix frontend
39+
# Copy pre-built frontend assets
40+
COPY --from=frontend-builder /workspace/pkg/plugin/assets ./pkg/plugin/assets
1941

20-
# Build the binary
21-
RUN CGO_ENABLED=0 GOOS=linux go build \
22-
-ldflags="-s -w" \
23-
-o bin/atest-ext-ai \
24-
./cmd/atest-ext-ai
42+
# Build the binary for the target platform
43+
RUN --mount=type=cache,target=/root/.cache/go-build \
44+
--mount=type=cache,target=/go/pkg/mod \
45+
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
46+
go build -ldflags="-s -w" -o bin/atest-ext-ai ./cmd/atest-ext-ai
2547

2648
# Final stage
2749
FROM alpine:latest

cmd/atest-ext-ai/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,8 @@ func cleanupSocketFile(path string) error {
287287
func createListener(cfg listenerConfig) (net.Listener, error) {
288288
if cfg.network == "unix" {
289289
dir := filepath.Dir(cfg.address)
290-
if err := os.MkdirAll(dir, 0o755); err != nil { //nolint:gosec // socket directory must remain accessible to API clients
290+
// #nosec G301 -- socket directory must remain accessible to API clients
291+
if err := os.MkdirAll(dir, 0o755); err != nil {
291292
return nil, fmt.Errorf("failed to create socket directory %s: %w", dir, err)
292293
}
293294

frontend/src/App.vue

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -152,61 +152,63 @@ async function handleTest(updatedConfig?: AIConfig) {
152152
display: flex;
153153
flex-direction: column;
154154
width: 100%;
155-
height: 100%;
156-
max-height: 100%;
155+
height: 100vh;
156+
max-height: 100vh;
157157
min-height: 0;
158158
padding: clamp(16px, 4vw, 32px);
159159
box-sizing: border-box;
160160
gap: clamp(12px, 2vw, 20px);
161161
background: var(--atest-bg-base);
162+
overflow: hidden;
162163
}
163164
164165
.welcome-panel {
165166
flex: 1;
166167
display: flex;
167168
align-items: center;
168169
justify-content: center;
170+
overflow: auto;
169171
}
170172
171173
.chat-content {
172174
flex: 1;
173-
display: grid;
174-
grid-template-rows: minmax(0, 1fr) auto;
175-
gap: var(--atest-spacing-md);
175+
display: flex;
176+
flex-direction: column;
177+
gap: 0;
176178
overflow: hidden;
177179
border-radius: var(--atest-radius-lg);
178180
background: var(--atest-bg-surface);
179181
box-shadow: var(--atest-shadow-md);
180-
padding: clamp(12px, 3vw, 24px);
181182
min-height: 0;
182183
max-height: 100%;
183184
}
184185
185186
@media (max-width: 1024px) {
186187
.ai-chat-container {
187188
padding: 24px;
189+
height: 100vh;
190+
max-height: 100vh;
188191
}
189192
}
190193
191194
@media (max-width: 768px) {
192195
.ai-chat-container {
193196
padding: 20px;
194197
gap: 12px;
198+
height: 100vh;
199+
max-height: 100vh;
195200
}
196201
197202
.chat-content {
198203
border-radius: var(--atest-radius-md);
199-
padding: 16px;
200204
}
201205
}
202206
203207
@media (max-width: 480px) {
204208
.ai-chat-container {
205209
padding: 16px;
206-
}
207-
208-
.chat-content {
209-
padding: 12px;
210+
height: 100vh;
211+
max-height: 100vh;
210212
}
211213
}
212214
</style>

frontend/src/components/AIChatHeader.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@ const statusText = computed(() => t(`ai.status.${props.status}`))
4141

4242
<style scoped>
4343
.ai-chat-header {
44+
flex-shrink: 0;
4445
display: flex;
4546
justify-content: space-between;
4647
align-items: center;
47-
padding: var(--atest-spacing-md) clamp(20px, 4vw, 28px);
48+
padding: var(--atest-spacing-md) clamp(20px, 4vw, 40px);
4849
background: var(--atest-bg-surface);
4950
border-bottom: 1px solid var(--atest-border-color);
5051
}

frontend/src/components/AIChatInput.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,10 @@ function handleSubmit() {
176176

177177
<style scoped>
178178
.chat-input {
179-
padding: 20px 40px 24px;
179+
flex-shrink: 0;
180+
padding: clamp(16px, 3vw, 20px) clamp(20px, 4vw, 40px) clamp(20px, 3vw, 24px);
180181
background: var(--atest-bg-surface);
181182
border-top: 1px solid var(--atest-border-color);
182-
box-shadow: var(--atest-shadow-sm);
183-
border-radius: var(--atest-radius-md);
184183
display: flex;
185184
flex-direction: column;
186185
gap: var(--atest-spacing-sm);

frontend/src/components/AIChatMessages.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,14 @@ async function copySQL(sql: string) {
123123

124124
<style scoped>
125125
.chat-messages {
126-
height: 100%;
126+
flex: 1;
127127
min-height: 0;
128128
overflow-y: auto;
129-
padding: 24px 40px;
129+
overflow-x: hidden;
130+
padding: clamp(12px, 3vw, 24px) clamp(20px, 4vw, 40px);
130131
background: var(--atest-bg-surface);
131132
color: var(--atest-text-primary);
132-
border: 1px solid var(--atest-border-color);
133-
border-radius: var(--atest-radius-md);
133+
border-bottom: 1px solid var(--atest-border-color);
134134
}
135135
136136
/* Empty state */

frontend/src/components/AISettingsPanel.vue

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,7 @@
144144
</el-option>
145145
</template>
146146
<template v-else>
147-
<el-option value="gpt-5" label="GPT-5 ⭐ Recommended" />
148-
<el-option value="gpt-5-mini" label="GPT-5 Mini" />
149-
<el-option value="gpt-5-nano" label="GPT-5 Nano" />
150-
<el-option value="gpt-5-pro" label="GPT-5 Pro" />
151-
<el-option value="gpt-4.1" label="GPT-4.1" />
147+
<el-option disabled value="no-models" :label="t('ai.welcome.noModels')" />
152148
</template>
153149
</el-select>
154150
<el-button
@@ -218,8 +214,7 @@
218214
</el-option>
219215
</template>
220216
<template v-else>
221-
<el-option value="deepseek-reasoner" label="DeepSeek Reasoner ⭐" />
222-
<el-option value="deepseek-chat" label="DeepSeek Chat" />
217+
<el-option disabled value="no-models" :label="t('ai.welcome.noModels')" />
223218
</template>
224219
</el-select>
225220
<el-button

frontend/src/composables/useAIChat.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ref, computed, watch } from 'vue'
22
import type { AppContext, AIConfig, Message, Model, DatabaseDialect } from '@/types'
3-
import { loadConfig, loadConfigForProvider, saveConfig, getMockModels, generateId, type Provider } from '@/utils/config'
3+
import { loadConfig, loadConfigForProvider, saveConfig, generateId, type Provider } from '@/utils/config'
44
import { aiService } from '@/services/aiService'
55

66
/**
@@ -27,6 +27,33 @@ export function useAIChat(_context: AppContext) {
2727
deepseek: []
2828
})
2929

30+
const catalogCache = ref<Record<string, Model[]>>({})
31+
32+
async function initializeModelCatalog() {
33+
try {
34+
const catalog = await aiService.fetchModelCatalog()
35+
const normalizedCatalog: Record<string, Model[]> = {}
36+
37+
Object.entries(catalog).forEach(([provider, entry]) => {
38+
normalizedCatalog[provider] = entry.models || []
39+
})
40+
41+
catalogCache.value = normalizedCatalog
42+
43+
for (const [provider, models] of Object.entries(normalizedCatalog)) {
44+
if (!modelsByProvider.value[provider]) {
45+
modelsByProvider.value[provider] = models
46+
} else if ((modelsByProvider.value[provider] || []).length === 0 && models.length > 0) {
47+
modelsByProvider.value[provider] = models
48+
}
49+
}
50+
} catch (error) {
51+
console.error('Failed to load model catalog', error)
52+
}
53+
}
54+
55+
void initializeModelCatalog()
56+
3057
// Computed property to get models for current provider
3158
const availableModels = computed(() => {
3259
const key = resolveProviderKey(config.value.provider)
@@ -98,8 +125,24 @@ export function useAIChat(_context: AppContext) {
98125
}
99126
} catch (error) {
100127
console.error('Failed to fetch models:', error)
101-
// Use mock models as fallback for this provider
102-
modelsByProvider.value[storeKey] = getMockModels(storeKey)
128+
const cachedFallback = catalogCache.value[storeKey]
129+
if (cachedFallback && cachedFallback.length) {
130+
modelsByProvider.value[storeKey] = cachedFallback
131+
return
132+
}
133+
134+
try {
135+
const catalog = await aiService.fetchModelCatalog(storeKey)
136+
const entry = catalog[storeKey]
137+
const fallbackModels = entry?.models ?? []
138+
modelsByProvider.value[storeKey] = fallbackModels
139+
if (fallbackModels.length) {
140+
catalogCache.value[storeKey] = fallbackModels
141+
}
142+
} catch (catalogError) {
143+
console.error('Failed to fetch catalog fallback:', catalogError)
144+
modelsByProvider.value[storeKey] = []
145+
}
103146
}
104147
}
105148

0 commit comments

Comments
 (0)