diff --git a/CHANGELOG.md b/CHANGELOG.md index 6214585e4..9699edbd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Shift+Tab navigates to the previous cell in the data grid - Copy rows writes TSV, HTML table, and plain text to the clipboard for richer paste in spreadsheet apps - Row drag adds TSV and HTML representations alongside the internal drag type +- AI provider settings allow manually entering a model name when the provider does not return one ### Changed diff --git a/TablePro/Views/Settings/AIProviderDetailSheet.swift b/TablePro/Views/Settings/AIProviderDetailSheet.swift index 330c03deb..1f41ceca9 100644 --- a/TablePro/Views/Settings/AIProviderDetailSheet.swift +++ b/TablePro/Views/Settings/AIProviderDetailSheet.swift @@ -72,7 +72,7 @@ struct AIProviderDetailSheet: View { ToolbarItem(placement: .confirmationAction) { Button(String(localized: "Save")) { cancelTasks() - onSave(draft, apiKey) + onSave(normalizedDraft, apiKey) } .keyboardShortcut(.defaultAction) .disabled(!isSaveEnabled) @@ -107,6 +107,12 @@ struct AIProviderDetailSheet: View { } } + private var normalizedDraft: AIProviderConfig { + var provider = draft + provider.model = draft.model.trimmingCharacters(in: .whitespacesAndNewlines) + return provider + } + // MARK: - Auth @ViewBuilder @@ -307,33 +313,34 @@ struct AIProviderDetailSheet: View { @ViewBuilder private var modelControl: some View { - if isFetchingModels { - ProgressView().controlSize(.small) - } else if fetchedModels.isEmpty { - HStack(spacing: 6) { - if !draft.model.isEmpty { - Text(draft.model) - .foregroundStyle(.secondary) - } + HStack(spacing: 8) { + TextField(String(localized: "Model name"), text: $draft.model) + .textFieldStyle(.roundedBorder) + .frame(width: 260) + + if isFetchingModels { + ProgressView().controlSize(.small) + } else if fetchedModels.isEmpty { Button(String(localized: "Reload")) { fetchModels() } .buttonStyle(.borderless) .controlSize(.small) - } - } else { - Picker("", selection: $draft.model) { - if draft.model.isEmpty { - Text(String(localized: "Select a model")).tag("") - } - ForEach(fetchedModels, id: \.self) { model in - Text(model).tag(model) + } else { + Menu { + ForEach(fetchedModels, id: \.self) { model in + Button(model) { + draft.model = model + } + } + } label: { + Image(systemName: "chevron.down.circle") } + .menuStyle(.borderlessButton) + .help(String(localized: "Choose a fetched model")) } - .labelsHidden() - .pickerStyle(.menu) - .fixedSize() } + .fixedSize() } // MARK: - Advanced @@ -436,7 +443,7 @@ struct AIProviderDetailSheet: View { return } - let provider = AIProviderFactory.createProvider(for: draft, apiKey: apiKey) + let provider = AIProviderFactory.createProvider(for: normalizedDraft, apiKey: apiKey) isFetchingModels = true modelFetchError = nil @@ -465,7 +472,7 @@ struct AIProviderDetailSheet: View { return } - let provider = AIProviderFactory.createProvider(for: draft, apiKey: apiKey) + let provider = AIProviderFactory.createProvider(for: normalizedDraft, apiKey: apiKey) isTesting = true testResult = nil diff --git a/docs/features/ai-assistant.mdx b/docs/features/ai-assistant.mdx index 786cc4557..b5d43de8b 100644 --- a/docs/features/ai-assistant.mdx +++ b/docs/features/ai-assistant.mdx @@ -28,7 +28,7 @@ Open **Settings** (`Cmd+,`) > **AI**. The tab is modeled on Xcode's Intelligence 1. Click **Add Provider...** and pick a type (GitHub Copilot, Claude, OpenAI, Gemini, OpenRouter, Ollama, or a custom OpenAI-compatible endpoint). 2. Enter the API key, or run device-flow sign-in for Copilot. -3. Pick a model. The list fetches automatically; click **Reload** if needed. +3. Enter a model name, or pick one from the fetched list. Click **Reload** if needed. 4. Click **Test Connection**. API keys are stored in the macOS Keychain. Ollama is detected at launch.