128 lines
3.7 KiB
Markdown
128 lines
3.7 KiB
Markdown
# DeepSeek API Integration with PocketBase React Apps
|
|
|
|
## Pattern
|
|
|
|
React frontend → nginx `/deepseek/` proxy → `https://api.deepseek.com/` with API key.
|
|
|
|
## Server Proxy (nginx)
|
|
|
|
```nginx
|
|
location /deepseek/ {
|
|
proxy_pass https://api.deepseek.com/;
|
|
proxy_ssl_server_name on;
|
|
proxy_ssl_name api.deepseek.com;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host api.deepseek.com;
|
|
proxy_set_header Authorization "Bearer sk-...";
|
|
proxy_set_header Content-Type "application/json";
|
|
proxy_buffering off;
|
|
proxy_read_timeout 120s;
|
|
}
|
|
```
|
|
|
|
## Client Module (`src/lib/ai.ts`)
|
|
|
|
Core fetch helper — all AI calls go through this:
|
|
|
|
```typescript
|
|
async function callDeepSeek(
|
|
systemPrompt: string,
|
|
userContent: string,
|
|
maxTokens = 500,
|
|
temperature = 0,
|
|
): Promise<string | null> {
|
|
const res = await fetch('/deepseek/v1/chat/completions', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: 'deepseek-v4-flash',
|
|
messages: [
|
|
{ role: 'system', content: systemPrompt },
|
|
{ role: 'user', content: userContent },
|
|
],
|
|
temperature,
|
|
thinking: { type: 'disabled' as const },
|
|
max_tokens: maxTokens,
|
|
}),
|
|
});
|
|
if (!res.ok) return null;
|
|
const data = await res.json();
|
|
return data?.choices?.[0]?.message?.content || null;
|
|
}
|
|
```
|
|
|
|
**Critical:** Set `thinking: { type: 'disabled' }` — without it, `deepseek-v4-flash` spends all token budget on reasoning content and returns empty `content`.
|
|
|
|
## Three Functions We Built
|
|
|
|
### 1. AI Write Explanation — generates professional customer-facing service descriptions
|
|
|
|
```typescript
|
|
export async function aiWriteExplanation(
|
|
params: ExplanationParams,
|
|
): Promise<ExplanationResult | null>
|
|
```
|
|
|
|
Takes `serviceName`, `recommendation`, optional `technicianNotes`, `vehicleInfo`, `mileage`, `maintenanceInterval`. Returns `{level: 'CRITICAL'|'RECOMMENDED'|'OPTIONAL'|'PREVENTIVE'|'MAINTENANCE', explanation: string}`.
|
|
|
|
Parses `LEVEL:` and `EXPLANATION:` from the AI response.
|
|
|
|
### 2. Generate Priorities — ranks services by safety criticality
|
|
|
|
```typescript
|
|
export async function getPriorityAnalysis(
|
|
services: ServiceItem[],
|
|
): Promise<PriorityResult[] | null>
|
|
```
|
|
|
|
Returns `{name, rank, priority_reason}[]` sorted by rank. AI prompt forces safety-first ordering: CRITICAL_SAFETY > SAFETY_CONCERN > RECOMMENDED > MAINTENANCE > OPTIONAL.
|
|
|
|
### 3. AI Suggest Services — recommends services from catalog based on vehicle + mileage
|
|
|
|
```typescript
|
|
export async function aiSuggestServices(
|
|
vehicle: SuggestVehicle,
|
|
selectedServices: string[],
|
|
catalog: CatalogItem[],
|
|
): Promise<SuggestResult[] | null>
|
|
```
|
|
|
|
Only returns services that exist in the provided catalog (cross-references by name). Uses mileage-based maintenance intervals.
|
|
|
|
## Response Parsing Safety
|
|
|
|
Always strip markdown code blocks before JSON parsing:
|
|
|
|
```typescript
|
|
function stripCodeBlocks(text: string): string {
|
|
return text.trim().replace(/```json\s*/gi, '').replace(/```\s*/g, '').trim();
|
|
}
|
|
```
|
|
|
|
All functions return `null` on failure (not throwing) so callers can handle gracefully with toast notifications.
|
|
|
|
## Fallback: Local Ollama
|
|
|
|
If the DeepSeek API is unavailable, proxy to Ollama instead. Change the proxy URL and model name:
|
|
|
|
```nginx
|
|
location /deepseek/ {
|
|
proxy_pass http://127.0.0.1:11434/;
|
|
...
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// In ai.ts
|
|
model: 'qwen2.5:14b', // or llava:13b
|
|
```
|
|
|
|
## UI Integration
|
|
|
|
Add Sparkles/ListOrdered/Lightbulb buttons to the service management UI. Each button:
|
|
1. Sets loading state (disables button, shows spinner)
|
|
2. Calls the AI function
|
|
3. On success: updates the service/quote state, shows success toast
|
|
4. On failure: shows error toast, re-enables button
|
|
5. Always: re-enables button in finally block
|