Migrate AI features to backend, add podcast generation

- ClaudeService: replace direct OpenRouter calls with backend endpoints
  (/v1/marks/enrich, /v1/marks/search, /v1/marks/collections)
- ClaudeService: add podcast methods (generate, status, downloadAudio)
- SettingsView: replace OpenRouter key field with backend URL + API key
- PodcastPlayerView: new sheet — generate → poll → AVPlayer playback
- BrowserView: headphones toolbar button triggers podcast for current URL
- BookmarksView: "Convert to Podcast" context menu item + sheet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krishna Kumar
2026-05-21 18:39:15 -05:00
parent ac2f9cf85d
commit bc16ee986d
6 changed files with 390 additions and 127 deletions

View File

@@ -4,25 +4,36 @@ struct SettingsView: View {
@Bindable var viewModel: BookmarksViewModel
let onDisconnect: () -> Void
@State private var claudeKey = UserDefaults.standard.string(forKey: ClaudeService.defaultsKey) ?? ""
@State private var backendUrl = UserDefaults.standard.string(forKey: ClaudeService.urlKey) ?? ""
@State private var claudeKey = UserDefaults.standard.string(forKey: ClaudeService.apiKeyKey) ?? ""
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
List {
Section("AI Features") {
Section {
VStack(alignment: .leading, spacing: 6) {
Text("OpenRouter API Key")
Text("Backend URL")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.secondary)
SecureField("sk-or-…", text: $claudeKey)
TextField("https://api.yourserver.com", text: $backendUrl)
.textContentType(.URL)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.keyboardType(.URL)
.onChange(of: backendUrl) { _, _ in reloadClaude() }
}
.padding(.vertical, 4)
VStack(alignment: .leading, spacing: 6) {
Text("API Key")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.secondary)
SecureField("Bearer token", text: $claudeKey)
.textContentType(.password)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.onChange(of: claudeKey) { _, new in
ClaudeService.save(apiKey: new)
viewModel.updateClaude(new.isEmpty ? nil : ClaudeService(apiKey: new))
}
.onChange(of: claudeKey) { _, _ in reloadClaude() }
}
.padding(.vertical, 4)
@@ -31,10 +42,12 @@ struct SettingsView: View {
.font(.system(size: 13))
.foregroundStyle(.secondary)
} else {
Text("Add an OpenRouter API key to enable AI features. Uses Gemini 2.0 Flash Lite — very cheap.")
Text("Enter your backend URL and API key to enable AI features.")
.font(.system(size: 13))
.foregroundStyle(.secondary)
}
} header: {
Text("AI Features")
}
Section("Server") {
@@ -55,4 +68,12 @@ struct SettingsView: View {
}
}
}
private func reloadClaude() {
ClaudeService.save(backendUrl: backendUrl, apiKey: claudeKey)
let service = backendUrl.isEmpty || claudeKey.isEmpty
? nil
: ClaudeService(backendUrl: backendUrl, apiKey: claudeKey)
viewModel.updateClaude(service)
}
}