/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { Code, Plus, Table, Trash2 } from 'lucide-react' import { useEffect, useId, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { JsonCodeEditor } from '@/components/json-code-editor' import { Alert, AlertDescription } from '@/components/ui/alert' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' type ModelMappingEditorProps = { value: string onChange: (value: string) => void disabled?: boolean sourceModelOptions?: string[] targetModelOptions?: string[] } type MappingRow = { id: string from: string to: string } const DUPLICATE_MAPPING_SENTINEL = '{ "duplicate_source_models": ' function getDuplicateSources(rows: MappingRow[]): string[] { const seen = new Set() const duplicates = new Set() for (const row of rows) { const source = row.from.trim() if (!source) continue if (seen.has(source)) { duplicates.add(source) } else { seen.add(source) } } return Array.from(duplicates) } export function ModelMappingEditor(props: ModelMappingEditorProps) { const { t } = useTranslation() const sourceListId = useId() const targetListId = useId() const [mode, setMode] = useState<'visual' | 'json'>('visual') const [rows, setRows] = useState([]) const [jsonValue, setJsonValue] = useState(props.value) const [jsonError, setJsonError] = useState(null) const nextRowIdRef = useRef(0) const duplicateSources = useMemo(() => getDuplicateSources(rows), [rows]) const createRowId = () => { nextRowIdRef.current += 1 return `mapping-${nextRowIdRef.current}` } const parseJsonToRows = (json: string): boolean => { try { if (!json.trim()) { setRows([]) setJsonError(null) return true } const parsed = JSON.parse(json) if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { setJsonError(t('Model mapping must be a valid JSON object')) return false } const entries = Object.entries(parsed) const invalidValue = entries.find(([, to]) => typeof to !== 'string') if (invalidValue) { setJsonError(t('Model mapping values must be strings')) return false } setRows((previousRows) => { const remainingRows = [...previousRows] return entries.map(([from, to], index) => { const toString = String(to) const existingIndex = remainingRows.findIndex( (row) => row.from === from || (row.from === from && row.to === toString) || previousRows[index]?.id === row.id ) if (existingIndex >= 0) { const [existing] = remainingRows.splice(existingIndex, 1) return { id: existing.id, from, to: toString, } } return { id: createRowId(), from, to: toString, } }) }) setJsonError(null) return true } catch (_error) { setJsonError(t('Model mapping must be valid JSON format')) return false } } // Parse JSON to rows when value changes externally useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect setJsonValue(props.value) parseJsonToRows(props.value) }, [props.value]) const convertRowsToJson = (updatedRows: MappingRow[]): string => { if (updatedRows.length === 0) { return '' } const obj: Record = {} updatedRows.forEach((row) => { if (row.from.trim()) { obj[row.from.trim()] = row.to.trim() } }) return JSON.stringify(obj, null, 2) } const syncRows = (updatedRows: MappingRow[]) => { setRows(updatedRows) const duplicates = getDuplicateSources(updatedRows) if (duplicates.length > 0) { setJsonError(t('Duplicate source model mappings are not allowed')) setJsonValue(DUPLICATE_MAPPING_SENTINEL) props.onChange(DUPLICATE_MAPPING_SENTINEL) return } const json = convertRowsToJson(updatedRows) setJsonError(null) setJsonValue(json) props.onChange(json) } const handleAddRow = () => { const newRow: MappingRow = { id: createRowId(), from: '', to: '', } syncRows([...rows, newRow]) } const handleDeleteRow = (id: string) => { syncRows(rows.filter((row) => row.id !== id)) } const handleRowChange = ( id: string, field: 'from' | 'to', newValue: string ) => { const updatedRows = rows.map((row) => row.id === id ? { ...row, [field]: newValue } : row ) syncRows(updatedRows) } const handleJsonChange = (newJson: string) => { setJsonValue(newJson) props.onChange(newJson) parseJsonToRows(newJson) } const handleFillTemplate = () => { const template = JSON.stringify( { 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125' }, null, 2 ) setJsonValue(template) props.onChange(template) parseJsonToRows(template) } const handleModeChange = (nextMode: string) => { if (nextMode !== 'visual' && nextMode !== 'json') return if (nextMode === 'json') { const duplicates = getDuplicateSources(rows) if (duplicates.length === 0) { const json = convertRowsToJson(rows) setJsonValue(json) props.onChange(json) } setMode('json') return } parseJsonToRows(jsonValue) setMode('visual') } return (
{t('Visual')}