Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,12 @@ import { useVectorModelStore } from "@/stores/vector-model-store";
import { useTheme } from "@/styles/theme";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { detectRemoteEmbeddingDimension } from "@readany/core/rag";
import type { VectorModelConfig } from "@readany/core/types";
import { Check, Cloud, Plus, Trash2, X } from "lucide-react-native";
import { useCallback, useState } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
Pressable,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
import { ActivityIndicator, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
import Animated, { SlideInRight } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import SearchSvg from "../../../../assets/illustrations/search.svg";
Expand All @@ -26,15 +20,11 @@ type NavProp = NativeStackNavigationProp<OnboardingStackParamList, "Embedding">;
export function EmbeddingPage() {
const { t } = useTranslation();
const navigation = useNavigation<NavProp>();
const { colors, isDark } = useTheme();
const { colors } = useTheme();
const insets = useSafeAreaInsets();

const {
vectorModels,
addVectorModel,
deleteVectorModel,
setSelectedVectorModelId,
} = useVectorModelStore();
const { vectorModels, addVectorModel, deleteVectorModel, setSelectedVectorModelId } =
useVectorModelStore();

const [showAddForm, setShowAddForm] = useState(false);
const [formData, setFormData] = useState({ name: "", url: "", modelId: "", apiKey: "" });
Expand All @@ -54,23 +44,8 @@ export function EmbeddingPage() {
const testRemoteModel = async (model: VectorModelConfig) => {
setTestingId(model.id);
try {
const testUrl = model.url.replace(/\/$/, "");
const isOllama = testUrl.endsWith("/api/embed");
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (model.apiKey?.trim()) headers.Authorization = `Bearer ${model.apiKey}`;

const requestBody = isOllama
? { model: model.modelId, input: "test" }
: { input: ["test"], model: model.modelId, encoding_format: "float" };

const res = await fetch(testUrl, {
method: "POST",
headers,
body: JSON.stringify(requestBody),
});
if (res.ok) {
setSelectedVectorModelId(model.id);
}
await detectRemoteEmbeddingDimension(model);
setSelectedVectorModelId(model.id);
} catch (err) {
console.warn("[Onboarding] Embedding model test failed:", err);
} finally {
Expand Down Expand Up @@ -273,9 +248,7 @@ export function EmbeddingPage() {
]}
>
<View style={styles.modelItemInfo}>
<Text style={[styles.modelItemName, { color: colors.foreground }]}>
{m.name}
</Text>
<Text style={[styles.modelItemName, { color: colors.foreground }]}>{m.name}</Text>
<Text style={[styles.modelItemMeta, { color: colors.mutedForeground }]}>
{m.modelId}
</Text>
Expand Down
92 changes: 55 additions & 37 deletions packages/app/src/components/onboarding/steps/EmbeddingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/ui/password-input";
import { Switch } from "@/components/ui/switch";
import { useVectorModelStore } from "@/stores/vector-model-store";
import { BUILTIN_EMBEDDING_MODELS } from "@readany/core/ai/builtin-embedding-models";
import { loadEmbeddingPipeline } from "@readany/core/ai/local-embedding-service";
import { detectRemoteEmbeddingDimension } from "@readany/core/rag";
import type { VectorModelConfig } from "@readany/core/types";
import { Check, Download, Loader2, Plus, Trash2, X } from "lucide-react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";

import { OnboardingLayout } from "../OnboardingLayout";

export function EmbeddingPage({ onNext, onPrev, step, totalSteps }: any) {
interface EmbeddingPageProps {
onNext: () => void;
onPrev: () => void;
step: number;
totalSteps: number;
}

export function EmbeddingPage({ onNext, onPrev, step, totalSteps }: EmbeddingPageProps) {
const { t } = useTranslation();
const {
vectorModelMode,
Expand Down Expand Up @@ -70,23 +77,8 @@ export function EmbeddingPage({ onNext, onPrev, step, totalSteps }: any) {
async (model: VectorModelConfig) => {
setTestingId(model.id);
try {
const testUrl = model.url.replace(/\/$/, "");
const isOllama = testUrl.endsWith("/api/embed");
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (model.apiKey?.trim()) headers.Authorization = `Bearer ${model.apiKey}`;

const requestBody = isOllama
? { model: model.modelId, input: "test" }
: { input: ["test"], model: model.modelId, encoding_format: "float" };

const res = await fetch(testUrl, {
method: "POST",
headers,
body: JSON.stringify(requestBody),
});
if (res.ok) {
setSelectedVectorModelId(model.id);
}
await detectRemoteEmbeddingDimension(model);
setSelectedVectorModelId(model.id);
} catch (err) {
console.warn("[Onboarding] Embedding model test failed:", err);
} finally {
Expand Down Expand Up @@ -141,8 +133,9 @@ export function EmbeddingPage({ onNext, onPrev, step, totalSteps }: any) {
</div>

<div className="grid grid-cols-2 gap-4 mt-6">
<div
className={`flex items-center justify-between rounded-lg border p-4 cursor-pointer transition-colors ${vectorModelMode === "remote" ? "border-primary bg-primary/5" : "border-border bg-muted/30"}`}
<button
type="button"
className={`flex items-center justify-between rounded-lg border p-4 cursor-pointer transition-colors text-left ${vectorModelMode === "remote" ? "border-primary bg-primary/5" : "border-border bg-muted/30"}`}
onClick={() => setVectorModelMode("remote")}
>
<div className="flex-1">
Expand All @@ -153,14 +146,19 @@ export function EmbeddingPage({ onNext, onPrev, step, totalSteps }: any) {
{t("onboarding.embedding.remoteDesc", "Connect to external embedding API.")}
</p>
</div>
<Switch
checked={vectorModelMode === "remote"}
onCheckedChange={(c) => setVectorModelMode(c ? "remote" : "builtin")}
/>
</div>
<span
aria-hidden="true"
className={`inline-flex h-5 w-9 shrink-0 items-center rounded-full border-2 border-transparent shadow-sm transition-colors ${vectorModelMode === "remote" ? "bg-primary" : "bg-input"}`}
>
<span
className={`block h-4 w-4 rounded-full bg-background shadow-lg transition-transform ${vectorModelMode === "remote" ? "translate-x-4" : "translate-x-0"}`}
/>
</span>
</button>

<div
className={`flex items-center justify-between rounded-lg border p-4 cursor-pointer transition-colors ${vectorModelMode === "builtin" ? "border-primary bg-primary/5" : "border-border bg-muted/30"}`}
<button
type="button"
className={`flex items-center justify-between rounded-lg border p-4 cursor-pointer transition-colors text-left ${vectorModelMode === "builtin" ? "border-primary bg-primary/5" : "border-border bg-muted/30"}`}
onClick={() => setVectorModelMode("builtin")}
>
<div className="flex-1">
Expand All @@ -171,11 +169,15 @@ export function EmbeddingPage({ onNext, onPrev, step, totalSteps }: any) {
{t("onboarding.embedding.localDesc", "Run embeddings safely on your device.")}
</p>
</div>
<Switch
checked={vectorModelMode === "builtin"}
onCheckedChange={(c) => setVectorModelMode(c ? "builtin" : "remote")}
/>
</div>
<span
aria-hidden="true"
className={`inline-flex h-5 w-9 shrink-0 items-center rounded-full border-2 border-transparent shadow-sm transition-colors ${vectorModelMode === "builtin" ? "bg-primary" : "bg-input"}`}
>
<span
className={`block h-4 w-4 rounded-full bg-background shadow-lg transition-transform ${vectorModelMode === "builtin" ? "translate-x-4" : "translate-x-0"}`}
/>
</span>
</button>
</div>

{vectorModelMode === "remote" && (
Expand Down Expand Up @@ -204,20 +206,28 @@ export function EmbeddingPage({ onNext, onPrev, step, totalSteps }: any) {

<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-muted-foreground mb-1 block">
<label
className="text-xs text-muted-foreground mb-1 block"
htmlFor="onboarding-embedding-name"
>
{t("settings.vm_name", "Name")} *
</label>
<Input
id="onboarding-embedding-name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="OpenAI Embedding"
/>
</div>
<div>
<label className="text-xs text-muted-foreground mb-1 block">
<label
className="text-xs text-muted-foreground mb-1 block"
htmlFor="onboarding-embedding-model-id"
>
{t("settings.vm_modelId", "Model ID")} *
</label>
<Input
id="onboarding-embedding-model-id"
value={formData.modelId}
onChange={(e) => setFormData({ ...formData, modelId: e.target.value })}
placeholder="text-embedding-3-small"
Expand All @@ -226,21 +236,29 @@ export function EmbeddingPage({ onNext, onPrev, step, totalSteps }: any) {
</div>

<div>
<label className="text-xs text-muted-foreground mb-1 block">
<label
className="text-xs text-muted-foreground mb-1 block"
htmlFor="onboarding-embedding-url"
>
{t("settings.vm_url", "URL")} *
</label>
<Input
id="onboarding-embedding-url"
value={formData.url}
onChange={(e) => setFormData({ ...formData, url: e.target.value })}
placeholder="https://api.openai.com/v1/embeddings"
/>
</div>

<div>
<label className="text-xs text-muted-foreground mb-1 block">
<label
className="text-xs text-muted-foreground mb-1 block"
htmlFor="onboarding-embedding-api-key"
>
{t("settings.vm_apiKey", "API Key")}
</label>
<PasswordInput
id="onboarding-embedding-api-key"
value={formData.apiKey}
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
placeholder="sk-..."
Expand Down
59 changes: 22 additions & 37 deletions packages/app/src/components/settings/VectorModelSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,15 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/ui/password-input";
import { Switch } from "@/components/ui/switch";
import { ConfigTransfer } from "./ConfigTransfer";
import { useVectorModelStore } from "@/stores/vector-model-store";
import { BUILTIN_EMBEDDING_MODELS } from "@readany/core/ai/builtin-embedding-models";
import { clearModelCache, loadEmbeddingPipeline } from "@readany/core/ai/local-embedding-service";
import { detectRemoteEmbeddingDimension } from "@readany/core/rag";
import type { VectorModelConfig } from "@readany/core/types";
import { Check, Download, Edit2, Loader2, Plus, Trash2, X } from "lucide-react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";

function normalizeEmbeddingsUrl(url: string): string {
return url.replace(/\/$/, "");
}
import { ConfigTransfer } from "./ConfigTransfer";

/* ------------------------------------------------------------------ */
/* Built-in Models Section */
Expand Down Expand Up @@ -266,26 +263,7 @@ function RemoteModelsSection() {
setTestingId(model.id);
setTestResults((prev) => ({ ...prev, [model.id]: t("settings.vm_testing") }));
try {
const testUrl = normalizeEmbeddingsUrl(model.url);
const isOllama = testUrl.endsWith("/api/embed");
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (model.apiKey.trim()) headers.Authorization = `Bearer ${model.apiKey}`;

const requestBody = isOllama
? { model: model.modelId, input: "test" }
: { input: ["test"], model: model.modelId, encoding_format: "float" };

const res = await fetch(testUrl, {
method: "POST",
headers,
body: JSON.stringify(requestBody),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const json = await res.json();
const len = isOllama
? (json?.embeddings?.[0]?.length ?? 0)
: (json?.data?.[0]?.embedding?.length ?? 0);

const len = await detectRemoteEmbeddingDimension(model);
updateVectorModel(model.id, { dimension: len });
setTestResults((prev) => ({
...prev,
Expand Down Expand Up @@ -432,21 +410,23 @@ function RemoteModelsSection() {

<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-xs text-muted-foreground">
<label className="mb-1 block text-xs text-muted-foreground" htmlFor="vm-name">
{t("settings.vm_name")} *
</label>
<Input
id="vm-name"
value={formData.name}
onChange={(e) => setFormData((p) => ({ ...p, name: e.target.value }))}
placeholder="OpenAI Embedding"
className="h-8 text-sm"
/>
</div>
<div>
<label className="mb-1 block text-xs text-muted-foreground">
<label className="mb-1 block text-xs text-muted-foreground" htmlFor="vm-model-id">
{t("settings.vm_modelId")} *
</label>
<Input
id="vm-model-id"
value={formData.modelId}
onChange={(e) => setFormData((p) => ({ ...p, modelId: e.target.value }))}
placeholder="text-embedding-3-small"
Expand All @@ -456,10 +436,11 @@ function RemoteModelsSection() {
</div>

<div>
<label className="mb-1 block text-xs text-muted-foreground">
<label className="mb-1 block text-xs text-muted-foreground" htmlFor="vm-url">
{t("settings.vm_url")} *
</label>
<Input
id="vm-url"
value={formData.url}
onChange={(e) => setFormData((p) => ({ ...p, url: e.target.value }))}
placeholder="https://api.openai.com/v1/embeddings"
Expand All @@ -469,10 +450,11 @@ function RemoteModelsSection() {
</div>

<div>
<label className="mb-1 block text-xs text-muted-foreground">
<label className="mb-1 block text-xs text-muted-foreground" htmlFor="vm-api-key">
{t("settings.vm_apiKey")}
</label>
<PasswordInput
id="vm-api-key"
value={formData.apiKey}
onChange={(e) => setFormData((p) => ({ ...p, apiKey: e.target.value }))}
placeholder="sk-..."
Expand All @@ -481,10 +463,11 @@ function RemoteModelsSection() {
</div>

<div>
<label className="mb-1 block text-xs text-muted-foreground">
<label className="mb-1 block text-xs text-muted-foreground" htmlFor="vm-description">
{t("settings.vm_description")}
</label>
<Input
id="vm-description"
value={formData.description}
onChange={(e) => setFormData((p) => ({ ...p, description: e.target.value }))}
placeholder={t("settings.vm_descriptionPlaceholder")}
Expand Down Expand Up @@ -603,14 +586,16 @@ export function VectorModelSettings() {
store.addVectorModel(m);
}
}
if (d.selectedVectorModelId) store.setSelectedVectorModelId(d.selectedVectorModelId as string);
if (typeof d.vectorModelEnabled === "boolean") store.setVectorModelEnabled(d.vectorModelEnabled);
if (d.vectorModelMode === "remote" || d.vectorModelMode === "builtin") store.setVectorModelMode(d.vectorModelMode);
if (d.selectedBuiltinModelId) store.setSelectedBuiltinModelId(d.selectedBuiltinModelId as string);
if (d.selectedVectorModelId)
store.setSelectedVectorModelId(d.selectedVectorModelId as string);
if (typeof d.vectorModelEnabled === "boolean")
store.setVectorModelEnabled(d.vectorModelEnabled);
if (d.vectorModelMode === "remote" || d.vectorModelMode === "builtin")
store.setVectorModelMode(d.vectorModelMode);
if (d.selectedBuiltinModelId)
store.setSelectedBuiltinModelId(d.selectedBuiltinModelId as string);
}}
validate={(d) =>
typeof d === "object" && d !== null && "vectorModels" in d
}
validate={(d) => typeof d === "object" && d !== null && "vectorModels" in d}
/>
</section>
</div>
Expand Down
Loading