Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Moved LLM settings (model, API key, API URL, parallel calls) from the DSW plugin settings to the service database.

## [0.3.0] - 2026-09-13

- Added Common templates and option to edit templates (#135)
Expand Down
78 changes: 57 additions & 21 deletions plugin/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { getApiUrlAndToken } from '@ds-wizard/plugin-sdk/requests'
import type { LanguageDefinition } from '@/data/languages'
import type {
CoverPagePreviewDefinition,
LlmSettings,
LlmSettingsUpdate,
PipelineStatusResponse,
PipelineSummaryItem,
TemplateDetail,
Expand Down Expand Up @@ -85,6 +87,8 @@ export const getAvailableLanguages = async (): Promise<LanguageDefinition[]> =>
return data as LanguageDefinition[]
}

const CONNECTION_ERROR_MESSAGE = 'Cannot connect to the server.'

const buildAuthHeaders = (): Record<string, string> => {
const { apiUrl, token } = getApiUrlAndToken()
if (!token) {
Expand All @@ -102,14 +106,22 @@ const buildAuthHeaders = (): Record<string, string> => {

// false positive for global type RequestInit
// eslint-disable-next-line no-undef
const apiFetch = (url: string, init?: RequestInit): Promise<Response> =>
fetch(url, {
...init,
headers: {
...buildAuthHeaders(),
...init?.headers,
},
})
const apiFetch = async (url: string, init?: RequestInit): Promise<Response> => {
const headers = {
...buildAuthHeaders(),
...init?.headers,
}

try {
return await fetch(url, { ...init, headers })
} catch (error) {
// fetch rejects with a TypeError ("Failed to fetch") when the request never gets a response.
if (error instanceof TypeError) {
throw new Error(CONNECTION_ERROR_MESSAGE, { cause: error })
}
throw error
}
}

export const getTemplates = async (): Promise<TemplateOption[]> => {
const url = `${getApiBaseUrl()}/templates`
Expand Down Expand Up @@ -205,20 +217,12 @@ type RunPipelineParams = {
questionnaireUuid: string
templateUuid: string
language: string
llmModel?: string | null
llmApiKey?: string | null
llmApiUrl?: string | null
llmMaxWorkers?: number | null
}

export const runPipeline = async ({
questionnaireUuid,
templateUuid,
language,
llmModel = null,
llmApiKey = null,
llmApiUrl = null,
llmMaxWorkers = null,
}: RunPipelineParams): Promise<PipelineStatusResponse> => {
const url = `${getApiBaseUrl()}/pipelines/run`
const response = await apiFetch(url, {
Expand All @@ -231,10 +235,6 @@ export const runPipeline = async ({
templateUuid,
language,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
llmModel,
llmApiKey,
llmApiUrl,
llmMaxWorkers,
}),
})

Expand All @@ -243,7 +243,7 @@ export const runPipeline = async ({
if (!response.ok) {
if (response.status == 422) {
throw new Error(
'Plugin is not configured. Set the model, API key, and API URL in the plugin settings.',
'Plugin is not configured. Ask your administrator to set the model, API key, and API URL in the plugin settings.',
)
}
const detail = 'detail' in data ? data.detail : undefined
Expand All @@ -259,6 +259,42 @@ export const runPipeline = async ({
return data
}

export const getLlmSettings = async (): Promise<LlmSettings> => {
const url = `${getApiBaseUrl()}/settings/llm`
const response = await apiFetch(url)
const data = await readApiResponse<LlmSettings | { detail?: string }>(response, url)

if (!response.ok) {
throw new Error(
'detail' in data && data.detail ? data.detail : 'Failed to load the LLM settings.',
)
}

return data as LlmSettings
}

export const updateLlmSettings = async (settings: LlmSettingsUpdate): Promise<LlmSettings> => {
const url = `${getApiBaseUrl()}/settings/llm`
const response = await apiFetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(settings),
})

const data = await readApiResponse<LlmSettings | { detail?: unknown }>(response, url)

if (!response.ok) {
const detail = 'detail' in data ? data.detail : undefined
throw new Error(
typeof detail === 'string' && detail ? detail : 'Failed to save the LLM settings.',
)
}

return data as LlmSettings
}

type CreateTemplateParams = {
hasCoverPage: boolean
title: string
Expand Down
2 changes: 1 addition & 1 deletion plugin/src/components/ProjectTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function ProjectTab({
reloadKey: settings.serviceUrl,
onLoadError: toast.error,
})
const history = useGenerationHistory(project, settings)
const history = useGenerationHistory(project)
const projectUuid = project?.uuid

const [selectedUuid, setSelectedUuid] = useState('')
Expand Down
5 changes: 0 additions & 5 deletions plugin/src/components/Settings.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,3 @@
display: grid;
gap: 1rem;
}

.lead {
margin: 0.5rem 0 0;
color: var(--ai-doc-color-slate-600);
}
81 changes: 3 additions & 78 deletions plugin/src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,90 +2,15 @@ import { SettingsComponentProps } from '@ds-wizard/plugin-sdk/elements'
import { Toaster } from 'sonner'

import styles from '@/components/Settings.module.css'
import { LlmSettingsSection } from '@/components/settings/LlmSettingsSection'
import { TenantTemplateSection } from '@/components/settings/TenantTemplateSettings'
import { SettingsData } from '@/data/settings-data'

export default function Settings({
settings,
onSettingsChange,
}: SettingsComponentProps<SettingsData>) {
export default function Settings(_props: SettingsComponentProps<SettingsData>) {
return (
<div className={styles.root}>
<Toaster richColors closeButton />
<div>
<p className={styles.lead}>
Configure the LLM connection the plugin should use for pipeline execution.
Please select a model that supports the OpenAI API to use this plugin. Default
values from config: model <code>gpt-oss-120b</code> and API URL{' '}
<code>https://llm.ai.e-infra.cz/v1/</code>.
</p>
</div>

<label className="ai-doc-field">
<span className="ai-doc-field-label">Model</span>
<input
type="text"
value={settings.model || ''}
onChange={(event) =>
onSettingsChange({
...settings,
model: event.target.value,
})
}
placeholder="gpt-4.1-mini"
className="ai-doc-input"
/>
</label>

<label className="ai-doc-field">
<span className="ai-doc-field-label">API key</span>
<input
type="password"
value={settings.apiKey || ''}
onChange={(event) =>
onSettingsChange({
...settings,
apiKey: event.target.value,
})
}
placeholder="sk-..."
className="ai-doc-input"
/>
</label>

<label className="ai-doc-field">
<span className="ai-doc-field-label">API URL</span>
<input
type="url"
value={settings.apiUrl || ''}
onChange={(event) =>
onSettingsChange({
...settings,
apiUrl: event.target.value,
})
}
placeholder="https://api.openai.com/v1"
className="ai-doc-input"
/>
</label>

<label className="ai-doc-field">
<span className="ai-doc-field-label">Maximum parallel calls to LLM server</span>
<input
type="number"
min={1}
value={settings.maxWorkers ?? ''}
onChange={(event) => {
const raw = event.target.value
onSettingsChange({
...settings,
maxWorkers:
raw === '' ? null : Math.max(1, Number.parseInt(raw, 10) || 1),
})
}}
className="ai-doc-input"
/>
</label>
<LlmSettingsSection />

<TenantTemplateSection />
</div>
Expand Down
19 changes: 19 additions & 0 deletions plugin/src/components/settings/LlmSettingsSection.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.root {
Comment thread
MatejFrnka marked this conversation as resolved.
Resolved
display: grid;
Comment thread
hranasit marked this conversation as resolved.
Resolved
gap: 1rem;
}

.heading {
margin: 0;
}

.muted {
margin: 0.35rem 0 0;
color: var(--ai-doc-color-slate-500);
line-height: 1.5;
}

.actions {
display: flex;
gap: 0.5rem;
}
Loading
Loading