feat(i18n): Integrate next-intl, localize core UI, add regional locales and zh-TW Discover sources
**Overview** - Integrates next-intl (App Router, no i18n routing) with cookie-based locale and Accept-Language fallback. - Adds message bundles and regional variants; sets en-US as the default. **Key changes** - i18n foundation - Adds request-scoped config to load messages per locale and injects NextIntlClientProvider in [layout.tsx] - Adds/updates messages for: en-US, en-GB, zh-TW, zh-HK, zh-CN, ja, ko, fr-FR, fr-CA, de. Centralizes LOCALES, LOCALE_LABELS, and DEFAULT_LOCALE in [locales.ts] - Adds LocaleSwitcher (cookie-based) and [LocaleBootstrap] - Pages and components - Localizes Sidebar, Home (including metadata/manifest), Settings, Discover, Library. - Localizes common components: MessageInput, Attach, Focus, Optimization, MessageBox, MessageSources, SearchImages, SearchVideos, EmptyChat, NewsArticleWidget, WeatherWidget. - APIs - Weather API returns localized condition strings server-side. - UX and quality - Converts all remaining <img> to Next Image. - Updates browserslist/caniuse DB to silence warnings. - Security: Settings API Key inputs are now password fields and placeholders were removed.
This commit is contained in:
parent
0dc17286b9
commit
9a772d6abe
56 changed files with 3673 additions and 365 deletions
|
|
@ -7,6 +7,7 @@ RUN yarn install --frozen-lockfile --network-timeout 600000
|
||||||
|
|
||||||
COPY tsconfig.json next.config.mjs next-env.d.ts postcss.config.js drizzle.config.ts tailwind.config.ts ./
|
COPY tsconfig.json next.config.mjs next-env.d.ts postcss.config.js drizzle.config.ts tailwind.config.ts ./
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
|
COPY messages ./messages
|
||||||
COPY public ./public
|
COPY public ./public
|
||||||
|
|
||||||
RUN mkdir -p /home/perplexica/data
|
RUN mkdir -p /home/perplexica/data
|
||||||
|
|
|
||||||
251
messages/de.json
Normal file
251
messages/de.json
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "Perplexica - Mit dem Internet chatten",
|
||||||
|
"description": "Perplexica ist ein KI-Chatbot, der mit dem Internet verbunden ist."
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "Perplexica - Mit dem Internet chatten",
|
||||||
|
"shortName": "Perplexica",
|
||||||
|
"description": "Perplexica ist ein KI-Chatbot, der mit dem Internet verbunden ist."
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "Startseite",
|
||||||
|
"discover": "Entdecken",
|
||||||
|
"library": "Bibliothek",
|
||||||
|
"settings": "Einstellungen"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"appName": "Perplexica",
|
||||||
|
"exportedOn": "Exportiert am:",
|
||||||
|
"citations": "Quellen:",
|
||||||
|
"user": "Benutzer",
|
||||||
|
"assistant": "Assistent",
|
||||||
|
"errors": {
|
||||||
|
"noChatModelsAvailable": "Keine Chat-Modelle verfügbar",
|
||||||
|
"chatProviderNotConfigured": "Es scheint, dass kein Chat-Model-Anbieter konfiguriert ist. Bitte konfigurieren Sie diesen auf der Einstellungsseite oder in der Konfigurationsdatei.",
|
||||||
|
"noEmbeddingModelsAvailable": "Keine Embedding-Modelle verfügbar",
|
||||||
|
"cannotSendBeforeConfigReady": "Nachricht kann nicht gesendet werden, bevor die Konfiguration abgeschlossen ist",
|
||||||
|
"failedToDeleteChat": "Chat konnte nicht gelöscht werden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"exportAsMarkdown": "Als Markdown exportieren",
|
||||||
|
"exportAsPDF": "Als PDF exportieren"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"chatExportTitle": "Chat-Export: {title}"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"conditions": {
|
||||||
|
"clear": "Klar",
|
||||||
|
"mainlyClear": "Überwiegend klar",
|
||||||
|
"partlyCloudy": "Teilweise bewölkt",
|
||||||
|
"cloudy": "Bewölkt",
|
||||||
|
"fog": "Nebel",
|
||||||
|
"lightDrizzle": "Leichter Nieselregen",
|
||||||
|
"moderateDrizzle": "Mäßiger Nieselregen",
|
||||||
|
"denseDrizzle": "Starker Nieselregen",
|
||||||
|
"lightFreezingDrizzle": "Leichter gefrierender Nieselregen",
|
||||||
|
"denseFreezingDrizzle": "Starker gefrierender Nieselregen",
|
||||||
|
"slightRain": "Leichter Regen",
|
||||||
|
"moderateRain": "Mäßiger Regen",
|
||||||
|
"heavyRain": "Starker Regen",
|
||||||
|
"lightFreezingRain": "Leichter gefrierender Regen",
|
||||||
|
"heavyFreezingRain": "Starker gefrierender Regen",
|
||||||
|
"slightSnowFall": "Leichter Schneefall",
|
||||||
|
"moderateSnowFall": "Mäßiger Schneefall",
|
||||||
|
"heavySnowFall": "Starker Schneefall",
|
||||||
|
"snow": "Schneefall",
|
||||||
|
"slightRainShowers": "Leichte Regenschauer",
|
||||||
|
"moderateRainShowers": "Regenschauer",
|
||||||
|
"heavyRainShowers": "Starke Regenschauer",
|
||||||
|
"slightSnowShowers": "Leichte Schneeschauer",
|
||||||
|
"moderateSnowShowers": "Schneeschauer",
|
||||||
|
"heavySnowShowers": "Starke Schneeschauer",
|
||||||
|
"thunderstorm": "Gewitter",
|
||||||
|
"thunderstormSlightHail": "Gewitter mit kleinem Hagel",
|
||||||
|
"thunderstormHeavyHail": "Gewitter mit starkem Hagel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"home": {
|
||||||
|
"title": "Chat - Perplexica",
|
||||||
|
"description": "Chatten Sie mit dem Internet, chatten Sie mit Perplexica."
|
||||||
|
},
|
||||||
|
"discover": {
|
||||||
|
"title": "Entdecken",
|
||||||
|
"topics": {
|
||||||
|
"tech": "Technik & Wissenschaft",
|
||||||
|
"finance": "Finanzen",
|
||||||
|
"art": "Kunst & Kultur",
|
||||||
|
"sports": "Sport",
|
||||||
|
"entertainment": "Unterhaltung"
|
||||||
|
},
|
||||||
|
"errorFetchingData": "Fehler beim Abrufen der Daten"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"title": "Bibliothek",
|
||||||
|
"empty": "Keine Chats gefunden.",
|
||||||
|
"ago": "vor {time}"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Einstellungen",
|
||||||
|
"sections": {
|
||||||
|
"preferences": "Voreinstellungen",
|
||||||
|
"automaticSearch": "Automatische Suche",
|
||||||
|
"systemInstructions": "Systemanweisungen",
|
||||||
|
"modelSettings": "Modelleinstellungen",
|
||||||
|
"apiKeys": "API-Schlüssel"
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"theme": "Theme",
|
||||||
|
"measurementUnits": "Maßeinheiten",
|
||||||
|
"language": "Sprache",
|
||||||
|
"metric": "Metrisch",
|
||||||
|
"imperial": "Imperial"
|
||||||
|
},
|
||||||
|
"automaticSearch": {
|
||||||
|
"image": {
|
||||||
|
"title": "Automatische Bildsuche",
|
||||||
|
"desc": "Automatisch relevante Bilder in Chat-Antworten suchen"
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"title": "Automatische Videosuche",
|
||||||
|
"desc": "Automatisch relevante Videos in Chat-Antworten suchen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"chatProvider": "Anbieter des Chat-Modells",
|
||||||
|
"chat": "Chat-Modell",
|
||||||
|
"noModels": "Keine Modelle verfügbar",
|
||||||
|
"invalidProvider": "Ungültiger Anbieter, bitte prüfen Sie die Backend-Logs",
|
||||||
|
"custom": {
|
||||||
|
"modelName": "Modellname",
|
||||||
|
"apiKey": "Benutzerdefinierter OpenAI API-Schlüssel",
|
||||||
|
"baseUrl": "Benutzerdefinierte OpenAI-Basis-URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"embedding": {
|
||||||
|
"provider": "Anbieter des Embedding-Modells",
|
||||||
|
"model": "Embedding-Modell"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"openaiApiKey": "OpenAI API-Schlüssel",
|
||||||
|
"ollamaApiUrl": "Ollama API-URL",
|
||||||
|
"groqApiKey": "GROQ API-Schlüssel",
|
||||||
|
"anthropicApiKey": "Anthropic API-Schlüssel",
|
||||||
|
"geminiApiKey": "Gemini API-Schlüssel",
|
||||||
|
"deepseekApiKey": "Deepseek API-Schlüssel",
|
||||||
|
"aimlApiKey": "AI/ML API-Schlüssel",
|
||||||
|
"lmStudioApiUrl": "LM Studio API-URL"
|
||||||
|
},
|
||||||
|
"systemInstructions": {
|
||||||
|
"placeholder": "Besondere Anweisungen für das LLM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"common": {
|
||||||
|
"viewMore": "Weitere {count} anzeigen"
|
||||||
|
},
|
||||||
|
"messageInput": {
|
||||||
|
"placeholder": "Rückfrage stellen"
|
||||||
|
},
|
||||||
|
"messageBox": {
|
||||||
|
"sources": "Quellen",
|
||||||
|
"answer": "Antwort",
|
||||||
|
"related": "Ähnlich"
|
||||||
|
},
|
||||||
|
"copilot": {
|
||||||
|
"label": "Copilot"
|
||||||
|
},
|
||||||
|
"attach": {
|
||||||
|
"attachedFiles": "Angehängte Dateien",
|
||||||
|
"add": "Hinzufügen",
|
||||||
|
"clear": "Leeren",
|
||||||
|
"attach": "Anhängen",
|
||||||
|
"uploading": "Hochladen...",
|
||||||
|
"files": "{count} Dateien"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"button": "Fokus",
|
||||||
|
"modes": {
|
||||||
|
"webSearch": {
|
||||||
|
"title": "Alle",
|
||||||
|
"description": "Im gesamten Internet suchen"
|
||||||
|
},
|
||||||
|
"academicSearch": {
|
||||||
|
"title": "Wissenschaftlich",
|
||||||
|
"description": "In veröffentlichten wissenschaftlichen Arbeiten suchen"
|
||||||
|
},
|
||||||
|
"writingAssistant": {
|
||||||
|
"title": "Schreiben",
|
||||||
|
"description": "Ohne Websuche chatten"
|
||||||
|
},
|
||||||
|
"wolframAlphaSearch": {
|
||||||
|
"title": "Wolfram Alpha",
|
||||||
|
"description": "Rechenwissen-Engine"
|
||||||
|
},
|
||||||
|
"youtubeSearch": {
|
||||||
|
"title": "YouTube",
|
||||||
|
"description": "Videos suchen und ansehen"
|
||||||
|
},
|
||||||
|
"redditSearch": {
|
||||||
|
"title": "Reddit",
|
||||||
|
"description": "Diskussionen und Meinungen suchen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"modes": {
|
||||||
|
"speed": {
|
||||||
|
"title": "Geschwindigkeit",
|
||||||
|
"description": "Geschwindigkeit priorisieren und schnellstmögliche Antwort erhalten"
|
||||||
|
},
|
||||||
|
"balanced": {
|
||||||
|
"title": "Ausgewogen",
|
||||||
|
"description": "Das richtige Gleichgewicht zwischen Geschwindigkeit und Genauigkeit finden"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"title": "Qualität (bald)",
|
||||||
|
"description": "Die gründlichste und genaueste Antwort erhalten"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageActions": {
|
||||||
|
"rewrite": "Neu formulieren"
|
||||||
|
},
|
||||||
|
"searchImages": {
|
||||||
|
"searchButton": "Bilder suchen"
|
||||||
|
},
|
||||||
|
"searchVideos": {
|
||||||
|
"searchButton": "Videos suchen",
|
||||||
|
"badge": "Video"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"humidity": "Luftfeuchtigkeit",
|
||||||
|
"now": "Jetzt"
|
||||||
|
},
|
||||||
|
"newsArticleWidget": {
|
||||||
|
"error": "News konnten nicht geladen werden."
|
||||||
|
},
|
||||||
|
"emptyChat": {
|
||||||
|
"title": "Forschung beginnt hier."
|
||||||
|
},
|
||||||
|
"emptyChatMessageInput": {
|
||||||
|
"placeholder": "Frag mich alles..."
|
||||||
|
},
|
||||||
|
"deleteChat": {
|
||||||
|
"title": "Löschbestätigung",
|
||||||
|
"description": "Möchten Sie diesen Chat wirklich löschen?",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"delete": "Löschen"
|
||||||
|
},
|
||||||
|
"themeSwitcher": {
|
||||||
|
"options": {
|
||||||
|
"light": "Hell",
|
||||||
|
"dark": "Dunkel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
messages/en-GB.json
Normal file
251
messages/en-GB.json
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "Perplexica - Chat with the internet",
|
||||||
|
"description": "Perplexica is an AI powered chatbot that is connected to the internet."
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "Perplexica - Chat with the internet",
|
||||||
|
"shortName": "Perplexica",
|
||||||
|
"description": "Perplexica is an AI powered chatbot that is connected to the internet."
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "Home",
|
||||||
|
"discover": "Discover",
|
||||||
|
"library": "Library",
|
||||||
|
"settings": "Settings"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"appName": "Perplexica",
|
||||||
|
"exportedOn": "Exported on:",
|
||||||
|
"citations": "Citations:",
|
||||||
|
"user": "User",
|
||||||
|
"assistant": "Assistant",
|
||||||
|
"errors": {
|
||||||
|
"noChatModelsAvailable": "No chat models available",
|
||||||
|
"chatProviderNotConfigured": "Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.",
|
||||||
|
"noEmbeddingModelsAvailable": "No embedding models available",
|
||||||
|
"cannotSendBeforeConfigReady": "Cannot send message before the configuration is ready",
|
||||||
|
"failedToDeleteChat": "Failed to delete chat"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"exportAsMarkdown": "Export as Markdown",
|
||||||
|
"exportAsPDF": "Export as PDF"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"chatExportTitle": "Chat Export: {title}"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"conditions": {
|
||||||
|
"clear": "Clear",
|
||||||
|
"mainlyClear": "Mainly Clear",
|
||||||
|
"partlyCloudy": "Partly Cloudy",
|
||||||
|
"cloudy": "Cloudy",
|
||||||
|
"fog": "Fog",
|
||||||
|
"lightDrizzle": "Light Drizzle",
|
||||||
|
"moderateDrizzle": "Moderate Drizzle",
|
||||||
|
"denseDrizzle": "Dense Drizzle",
|
||||||
|
"lightFreezingDrizzle": "Light Freezing Drizzle",
|
||||||
|
"denseFreezingDrizzle": "Dense Freezing Drizzle",
|
||||||
|
"slightRain": "Slight Rain",
|
||||||
|
"moderateRain": "Moderate Rain",
|
||||||
|
"heavyRain": "Heavy Rain",
|
||||||
|
"lightFreezingRain": "Light Freezing Rain",
|
||||||
|
"heavyFreezingRain": "Heavy Freezing Rain",
|
||||||
|
"slightSnowFall": "Slight Snow Fall",
|
||||||
|
"moderateSnowFall": "Moderate Snow Fall",
|
||||||
|
"heavySnowFall": "Heavy Snow Fall",
|
||||||
|
"snow": "Snow",
|
||||||
|
"slightRainShowers": "Slight Rain Showers",
|
||||||
|
"moderateRainShowers": "Moderate Rain Showers",
|
||||||
|
"heavyRainShowers": "Heavy Rain Showers",
|
||||||
|
"slightSnowShowers": "Slight Snow Showers",
|
||||||
|
"moderateSnowShowers": "Moderate Snow Showers",
|
||||||
|
"heavySnowShowers": "Heavy Snow Showers",
|
||||||
|
"thunderstorm": "Thunderstorm",
|
||||||
|
"thunderstormSlightHail": "Thunderstorm with Slight Hail",
|
||||||
|
"thunderstormHeavyHail": "Thunderstorm with Heavy Hail"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"home": {
|
||||||
|
"title": "Chat - Perplexica",
|
||||||
|
"description": "Chat with the internet, chat with Perplexica."
|
||||||
|
},
|
||||||
|
"discover": {
|
||||||
|
"title": "Discover",
|
||||||
|
"topics": {
|
||||||
|
"tech": "Tech & Science",
|
||||||
|
"finance": "Finance",
|
||||||
|
"art": "Art & Culture",
|
||||||
|
"sports": "Sports",
|
||||||
|
"entertainment": "Entertainment"
|
||||||
|
},
|
||||||
|
"errorFetchingData": "Error fetching data"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"title": "Library",
|
||||||
|
"empty": "No chats found.",
|
||||||
|
"ago": "{time} Ago"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"sections": {
|
||||||
|
"preferences": "Preferences",
|
||||||
|
"automaticSearch": "Automatic Search",
|
||||||
|
"systemInstructions": "System Instructions",
|
||||||
|
"modelSettings": "Model Settings",
|
||||||
|
"apiKeys": "API Keys"
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"theme": "Theme",
|
||||||
|
"measurementUnits": "Measurement Units",
|
||||||
|
"language": "Language",
|
||||||
|
"metric": "Metric",
|
||||||
|
"imperial": "Imperial"
|
||||||
|
},
|
||||||
|
"automaticSearch": {
|
||||||
|
"image": {
|
||||||
|
"title": "Automatic Image Search",
|
||||||
|
"desc": "Automatically search for relevant images in chat responses"
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"title": "Automatic Video Search",
|
||||||
|
"desc": "Automatically search for relevant videos in chat responses"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"chatProvider": "Chat Model Provider",
|
||||||
|
"chat": "Chat Model",
|
||||||
|
"noModels": "No models available",
|
||||||
|
"invalidProvider": "Invalid provider, please check backend logs",
|
||||||
|
"custom": {
|
||||||
|
"modelName": "Model Name",
|
||||||
|
"apiKey": "Custom OpenAI API Key",
|
||||||
|
"baseUrl": "Custom OpenAI Base URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"embedding": {
|
||||||
|
"provider": "Embedding Model Provider",
|
||||||
|
"model": "Embedding Model"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"openaiApiKey": "OpenAI API Key",
|
||||||
|
"ollamaApiUrl": "Ollama API URL",
|
||||||
|
"groqApiKey": "GROQ API Key",
|
||||||
|
"anthropicApiKey": "Anthropic API Key",
|
||||||
|
"geminiApiKey": "Gemini API Key",
|
||||||
|
"deepseekApiKey": "Deepseek API Key",
|
||||||
|
"aimlApiKey": "AI/ML API Key",
|
||||||
|
"lmStudioApiUrl": "LM Studio API URL"
|
||||||
|
},
|
||||||
|
"systemInstructions": {
|
||||||
|
"placeholder": "Any special instructions for the LLM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"common": {
|
||||||
|
"viewMore": "View {count} more"
|
||||||
|
},
|
||||||
|
"messageInput": {
|
||||||
|
"placeholder": "Ask a follow-up"
|
||||||
|
},
|
||||||
|
"messageBox": {
|
||||||
|
"sources": "Sources",
|
||||||
|
"answer": "Answer",
|
||||||
|
"related": "Related"
|
||||||
|
},
|
||||||
|
"copilot": {
|
||||||
|
"label": "Copilot"
|
||||||
|
},
|
||||||
|
"attach": {
|
||||||
|
"attachedFiles": "Attached files",
|
||||||
|
"add": "Add",
|
||||||
|
"clear": "Clear",
|
||||||
|
"attach": "Attach",
|
||||||
|
"uploading": "Uploading...",
|
||||||
|
"files": "{count} files"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"button": "Focus",
|
||||||
|
"modes": {
|
||||||
|
"webSearch": {
|
||||||
|
"title": "All",
|
||||||
|
"description": "Searches across all of the internet"
|
||||||
|
},
|
||||||
|
"academicSearch": {
|
||||||
|
"title": "Academic",
|
||||||
|
"description": "Search in published academic papers"
|
||||||
|
},
|
||||||
|
"writingAssistant": {
|
||||||
|
"title": "Writing",
|
||||||
|
"description": "Chat without searching the web"
|
||||||
|
},
|
||||||
|
"wolframAlphaSearch": {
|
||||||
|
"title": "Wolfram Alpha",
|
||||||
|
"description": "Computational knowledge engine"
|
||||||
|
},
|
||||||
|
"youtubeSearch": {
|
||||||
|
"title": "YouTube",
|
||||||
|
"description": "Search and watch videos"
|
||||||
|
},
|
||||||
|
"redditSearch": {
|
||||||
|
"title": "Reddit",
|
||||||
|
"description": "Search for discussions and opinions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"modes": {
|
||||||
|
"speed": {
|
||||||
|
"title": "Speed",
|
||||||
|
"description": "Prioritize speed and get the quickest possible answer."
|
||||||
|
},
|
||||||
|
"balanced": {
|
||||||
|
"title": "Balanced",
|
||||||
|
"description": "Find the right balance between speed and accuracy"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"title": "Quality (Soon)",
|
||||||
|
"description": "Get the most thorough and accurate answer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageActions": {
|
||||||
|
"rewrite": "Rewrite"
|
||||||
|
},
|
||||||
|
"searchImages": {
|
||||||
|
"searchButton": "Search images"
|
||||||
|
},
|
||||||
|
"searchVideos": {
|
||||||
|
"searchButton": "Search videos",
|
||||||
|
"badge": "Video"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"humidity": "Humidity",
|
||||||
|
"now": "Now"
|
||||||
|
},
|
||||||
|
"newsArticleWidget": {
|
||||||
|
"error": "Could not load news."
|
||||||
|
},
|
||||||
|
"emptyChat": {
|
||||||
|
"title": "Research begins here."
|
||||||
|
},
|
||||||
|
"emptyChatMessageInput": {
|
||||||
|
"placeholder": "Ask anything..."
|
||||||
|
},
|
||||||
|
"deleteChat": {
|
||||||
|
"title": "Delete Confirmation",
|
||||||
|
"description": "Are you sure you want to delete this chat?",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"delete": "Delete"
|
||||||
|
},
|
||||||
|
"themeSwitcher": {
|
||||||
|
"options": {
|
||||||
|
"light": "Light",
|
||||||
|
"dark": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
messages/en-US.json
Normal file
251
messages/en-US.json
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "Perplexica - Chat with the internet",
|
||||||
|
"description": "Perplexica is an AI powered chatbot that is connected to the internet."
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "Perplexica - Chat with the internet",
|
||||||
|
"shortName": "Perplexica",
|
||||||
|
"description": "Perplexica is an AI powered chatbot that is connected to the internet."
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "Home",
|
||||||
|
"discover": "Discover",
|
||||||
|
"library": "Library",
|
||||||
|
"settings": "Settings"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"appName": "Perplexica",
|
||||||
|
"exportedOn": "Exported on:",
|
||||||
|
"citations": "Citations:",
|
||||||
|
"user": "User",
|
||||||
|
"assistant": "Assistant",
|
||||||
|
"errors": {
|
||||||
|
"noChatModelsAvailable": "No chat models available",
|
||||||
|
"chatProviderNotConfigured": "Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.",
|
||||||
|
"noEmbeddingModelsAvailable": "No embedding models available",
|
||||||
|
"cannotSendBeforeConfigReady": "Cannot send message before the configuration is ready",
|
||||||
|
"failedToDeleteChat": "Failed to delete chat"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"exportAsMarkdown": "Export as Markdown",
|
||||||
|
"exportAsPDF": "Export as PDF"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"chatExportTitle": "Chat Export: {title}"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"conditions": {
|
||||||
|
"clear": "Clear",
|
||||||
|
"mainlyClear": "Mainly Clear",
|
||||||
|
"partlyCloudy": "Partly Cloudy",
|
||||||
|
"cloudy": "Cloudy",
|
||||||
|
"fog": "Fog",
|
||||||
|
"lightDrizzle": "Light Drizzle",
|
||||||
|
"moderateDrizzle": "Moderate Drizzle",
|
||||||
|
"denseDrizzle": "Dense Drizzle",
|
||||||
|
"lightFreezingDrizzle": "Light Freezing Drizzle",
|
||||||
|
"denseFreezingDrizzle": "Dense Freezing Drizzle",
|
||||||
|
"slightRain": "Slight Rain",
|
||||||
|
"moderateRain": "Moderate Rain",
|
||||||
|
"heavyRain": "Heavy Rain",
|
||||||
|
"lightFreezingRain": "Light Freezing Rain",
|
||||||
|
"heavyFreezingRain": "Heavy Freezing Rain",
|
||||||
|
"slightSnowFall": "Slight Snow Fall",
|
||||||
|
"moderateSnowFall": "Moderate Snow Fall",
|
||||||
|
"heavySnowFall": "Heavy Snow Fall",
|
||||||
|
"snow": "Snow",
|
||||||
|
"slightRainShowers": "Slight Rain Showers",
|
||||||
|
"moderateRainShowers": "Moderate Rain Showers",
|
||||||
|
"heavyRainShowers": "Heavy Rain Showers",
|
||||||
|
"slightSnowShowers": "Slight Snow Showers",
|
||||||
|
"moderateSnowShowers": "Moderate Snow Showers",
|
||||||
|
"heavySnowShowers": "Heavy Snow Showers",
|
||||||
|
"thunderstorm": "Thunderstorm",
|
||||||
|
"thunderstormSlightHail": "Thunderstorm with Slight Hail",
|
||||||
|
"thunderstormHeavyHail": "Thunderstorm with Heavy Hail"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"home": {
|
||||||
|
"title": "Chat - Perplexica",
|
||||||
|
"description": "Chat with the internet, chat with Perplexica."
|
||||||
|
},
|
||||||
|
"discover": {
|
||||||
|
"title": "Discover",
|
||||||
|
"topics": {
|
||||||
|
"tech": "Tech & Science",
|
||||||
|
"finance": "Finance",
|
||||||
|
"art": "Art & Culture",
|
||||||
|
"sports": "Sports",
|
||||||
|
"entertainment": "Entertainment"
|
||||||
|
},
|
||||||
|
"errorFetchingData": "Error fetching data"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"title": "Library",
|
||||||
|
"empty": "No chats found.",
|
||||||
|
"ago": "{time} Ago"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"sections": {
|
||||||
|
"preferences": "Preferences",
|
||||||
|
"automaticSearch": "Automatic Search",
|
||||||
|
"systemInstructions": "System Instructions",
|
||||||
|
"modelSettings": "Model Settings",
|
||||||
|
"apiKeys": "API Keys"
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"theme": "Theme",
|
||||||
|
"measurementUnits": "Measurement Units",
|
||||||
|
"language": "Language",
|
||||||
|
"metric": "Metric",
|
||||||
|
"imperial": "Imperial"
|
||||||
|
},
|
||||||
|
"automaticSearch": {
|
||||||
|
"image": {
|
||||||
|
"title": "Automatic Image Search",
|
||||||
|
"desc": "Automatically search for relevant images in chat responses"
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"title": "Automatic Video Search",
|
||||||
|
"desc": "Automatically search for relevant videos in chat responses"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"chatProvider": "Chat Model Provider",
|
||||||
|
"chat": "Chat Model",
|
||||||
|
"noModels": "No models available",
|
||||||
|
"invalidProvider": "Invalid provider, please check backend logs",
|
||||||
|
"custom": {
|
||||||
|
"modelName": "Model Name",
|
||||||
|
"apiKey": "Custom OpenAI API Key",
|
||||||
|
"baseUrl": "Custom OpenAI Base URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"embedding": {
|
||||||
|
"provider": "Embedding Model Provider",
|
||||||
|
"model": "Embedding Model"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"openaiApiKey": "OpenAI API Key",
|
||||||
|
"ollamaApiUrl": "Ollama API URL",
|
||||||
|
"groqApiKey": "GROQ API Key",
|
||||||
|
"anthropicApiKey": "Anthropic API Key",
|
||||||
|
"geminiApiKey": "Gemini API Key",
|
||||||
|
"deepseekApiKey": "Deepseek API Key",
|
||||||
|
"aimlApiKey": "AI/ML API Key",
|
||||||
|
"lmStudioApiUrl": "LM Studio API URL"
|
||||||
|
},
|
||||||
|
"systemInstructions": {
|
||||||
|
"placeholder": "Any special instructions for the LLM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"common": {
|
||||||
|
"viewMore": "View {count} more"
|
||||||
|
},
|
||||||
|
"messageInput": {
|
||||||
|
"placeholder": "Ask a follow-up"
|
||||||
|
},
|
||||||
|
"messageBox": {
|
||||||
|
"sources": "Sources",
|
||||||
|
"answer": "Answer",
|
||||||
|
"related": "Related"
|
||||||
|
},
|
||||||
|
"copilot": {
|
||||||
|
"label": "Copilot"
|
||||||
|
},
|
||||||
|
"attach": {
|
||||||
|
"attachedFiles": "Attached files",
|
||||||
|
"add": "Add",
|
||||||
|
"clear": "Clear",
|
||||||
|
"attach": "Attach",
|
||||||
|
"uploading": "Uploading...",
|
||||||
|
"files": "{count} files"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"button": "Focus",
|
||||||
|
"modes": {
|
||||||
|
"webSearch": {
|
||||||
|
"title": "All",
|
||||||
|
"description": "Searches across all of the internet"
|
||||||
|
},
|
||||||
|
"academicSearch": {
|
||||||
|
"title": "Academic",
|
||||||
|
"description": "Search in published academic papers"
|
||||||
|
},
|
||||||
|
"writingAssistant": {
|
||||||
|
"title": "Writing",
|
||||||
|
"description": "Chat without searching the web"
|
||||||
|
},
|
||||||
|
"wolframAlphaSearch": {
|
||||||
|
"title": "Wolfram Alpha",
|
||||||
|
"description": "Computational knowledge engine"
|
||||||
|
},
|
||||||
|
"youtubeSearch": {
|
||||||
|
"title": "YouTube",
|
||||||
|
"description": "Search and watch videos"
|
||||||
|
},
|
||||||
|
"redditSearch": {
|
||||||
|
"title": "Reddit",
|
||||||
|
"description": "Search for discussions and opinions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"modes": {
|
||||||
|
"speed": {
|
||||||
|
"title": "Speed",
|
||||||
|
"description": "Prioritize speed and get the quickest possible answer."
|
||||||
|
},
|
||||||
|
"balanced": {
|
||||||
|
"title": "Balanced",
|
||||||
|
"description": "Find the right balance between speed and accuracy"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"title": "Quality (Soon)",
|
||||||
|
"description": "Get the most thorough and accurate answer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageActions": {
|
||||||
|
"rewrite": "Rewrite"
|
||||||
|
},
|
||||||
|
"searchImages": {
|
||||||
|
"searchButton": "Search images"
|
||||||
|
},
|
||||||
|
"searchVideos": {
|
||||||
|
"searchButton": "Search videos",
|
||||||
|
"badge": "Video"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"humidity": "Humidity",
|
||||||
|
"now": "Now"
|
||||||
|
},
|
||||||
|
"newsArticleWidget": {
|
||||||
|
"error": "Could not load news."
|
||||||
|
},
|
||||||
|
"emptyChat": {
|
||||||
|
"title": "Research begins here."
|
||||||
|
},
|
||||||
|
"emptyChatMessageInput": {
|
||||||
|
"placeholder": "Ask anything..."
|
||||||
|
},
|
||||||
|
"deleteChat": {
|
||||||
|
"title": "Delete Confirmation",
|
||||||
|
"description": "Are you sure you want to delete this chat?",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"delete": "Delete"
|
||||||
|
},
|
||||||
|
"themeSwitcher": {
|
||||||
|
"options": {
|
||||||
|
"light": "Light",
|
||||||
|
"dark": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
messages/fr-CA.json
Normal file
251
messages/fr-CA.json
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "Perplexica - Jaser avec Internet",
|
||||||
|
"description": "Perplexica est un chatbot IA branché sur Internet."
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "Perplexica - Jaser avec Internet",
|
||||||
|
"shortName": "Perplexica",
|
||||||
|
"description": "Perplexica est un chatbot IA branché sur Internet."
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "Accueil",
|
||||||
|
"discover": "Découvrir",
|
||||||
|
"library": "Bibliothèque",
|
||||||
|
"settings": "Paramètres"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"appName": "Perplexica",
|
||||||
|
"exportedOn": "Exporté le :",
|
||||||
|
"citations": "Citations :",
|
||||||
|
"user": "Utilisateur",
|
||||||
|
"assistant": "Assistant",
|
||||||
|
"errors": {
|
||||||
|
"noChatModelsAvailable": "Aucun modèle de chat disponible",
|
||||||
|
"chatProviderNotConfigured": "Aucun fournisseur de modèle de chat n'est configuré. Veuillez le configurer à partir de la page des paramètres ou du fichier de configuration.",
|
||||||
|
"noEmbeddingModelsAvailable": "Aucun modèle d'embedding disponible",
|
||||||
|
"cannotSendBeforeConfigReady": "Impossible d'envoyer un message avant la fin de la configuration",
|
||||||
|
"failedToDeleteChat": "Échec de la suppression du chat"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"exportAsMarkdown": "Exporter en Markdown",
|
||||||
|
"exportAsPDF": "Exporter en PDF"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"chatExportTitle": "Export de conversation : {title}"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"conditions": {
|
||||||
|
"clear": "Dégagé",
|
||||||
|
"mainlyClear": "Plutôt dégagé",
|
||||||
|
"partlyCloudy": "Partiellement nuageux",
|
||||||
|
"cloudy": "Nuageux",
|
||||||
|
"fog": "Brouillard",
|
||||||
|
"lightDrizzle": "Bruine faible",
|
||||||
|
"moderateDrizzle": "Bruine modérée",
|
||||||
|
"denseDrizzle": "Bruine forte",
|
||||||
|
"lightFreezingDrizzle": "Bruine verglaçante faible",
|
||||||
|
"denseFreezingDrizzle": "Bruine verglaçante forte",
|
||||||
|
"slightRain": "Pluie faible",
|
||||||
|
"moderateRain": "Pluie modérée",
|
||||||
|
"heavyRain": "Pluie forte",
|
||||||
|
"lightFreezingRain": "Pluie verglaçante faible",
|
||||||
|
"heavyFreezingRain": "Pluie verglaçante forte",
|
||||||
|
"slightSnowFall": "Neige faible",
|
||||||
|
"moderateSnowFall": "Neige modérée",
|
||||||
|
"heavySnowFall": "Fortes chutes de neige",
|
||||||
|
"snow": "Chute de neige",
|
||||||
|
"slightRainShowers": "Averses faibles",
|
||||||
|
"moderateRainShowers": "Averses",
|
||||||
|
"heavyRainShowers": "Fortes averses",
|
||||||
|
"slightSnowShowers": "Averses de neige faibles",
|
||||||
|
"moderateSnowShowers": "Averses de neige",
|
||||||
|
"heavySnowShowers": "Fortes averses de neige",
|
||||||
|
"thunderstorm": "Orage",
|
||||||
|
"thunderstormSlightHail": "Orage avec petites grêles",
|
||||||
|
"thunderstormHeavyHail": "Orage avec fortes grêles"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"home": {
|
||||||
|
"title": "Chat - Perplexica",
|
||||||
|
"description": "Jasez avec Internet, jasez avec Perplexica."
|
||||||
|
},
|
||||||
|
"discover": {
|
||||||
|
"title": "Découvrir",
|
||||||
|
"topics": {
|
||||||
|
"tech": "Tech et science",
|
||||||
|
"finance": "Finance",
|
||||||
|
"art": "Art et culture",
|
||||||
|
"sports": "Sports",
|
||||||
|
"entertainment": "Divertissement"
|
||||||
|
},
|
||||||
|
"errorFetchingData": "Erreur lors de la récupération des données"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"title": "Bibliothèque",
|
||||||
|
"empty": "Aucune conversation trouvée.",
|
||||||
|
"ago": "Il y a {time}"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Paramètres",
|
||||||
|
"sections": {
|
||||||
|
"preferences": "Préférences",
|
||||||
|
"automaticSearch": "Recherche automatique",
|
||||||
|
"systemInstructions": "Instructions système",
|
||||||
|
"modelSettings": "Paramètres du modèle",
|
||||||
|
"apiKeys": "Clés API"
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"theme": "Thème",
|
||||||
|
"measurementUnits": "Unités de mesure",
|
||||||
|
"language": "Langue",
|
||||||
|
"metric": "Métrique",
|
||||||
|
"imperial": "Impérial"
|
||||||
|
},
|
||||||
|
"automaticSearch": {
|
||||||
|
"image": {
|
||||||
|
"title": "Recherche d'images automatique",
|
||||||
|
"desc": "Rechercher automatiquement des images pertinentes dans les réponses du chat"
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"title": "Recherche de vidéos automatique",
|
||||||
|
"desc": "Rechercher automatiquement des vidéos pertinentes dans les réponses du chat"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"chatProvider": "Fournisseur du modèle de chat",
|
||||||
|
"chat": "Modèle de chat",
|
||||||
|
"noModels": "Aucun modèle disponible",
|
||||||
|
"invalidProvider": "Fournisseur invalide, veuillez vérifier les journaux du backend",
|
||||||
|
"custom": {
|
||||||
|
"modelName": "Nom du modèle",
|
||||||
|
"apiKey": "Clé API OpenAI personnalisée",
|
||||||
|
"baseUrl": "URL de base OpenAI personnalisée"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"embedding": {
|
||||||
|
"provider": "Fournisseur du modèle d'embedding",
|
||||||
|
"model": "Modèle d'embedding"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"openaiApiKey": "Clé API OpenAI",
|
||||||
|
"ollamaApiUrl": "URL de l'API Ollama",
|
||||||
|
"groqApiKey": "Clé API GROQ",
|
||||||
|
"anthropicApiKey": "Clé API Anthropic",
|
||||||
|
"geminiApiKey": "Clé API Gemini",
|
||||||
|
"deepseekApiKey": "Clé API Deepseek",
|
||||||
|
"aimlApiKey": "Clé API AI/ML",
|
||||||
|
"lmStudioApiUrl": "URL de l'API LM Studio"
|
||||||
|
},
|
||||||
|
"systemInstructions": {
|
||||||
|
"placeholder": "Toute instruction spécifique pour le LLM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"common": {
|
||||||
|
"viewMore": "Voir {count} de plus"
|
||||||
|
},
|
||||||
|
"messageInput": {
|
||||||
|
"placeholder": "Poser une question de suivi"
|
||||||
|
},
|
||||||
|
"messageBox": {
|
||||||
|
"sources": "Sources",
|
||||||
|
"answer": "Réponse",
|
||||||
|
"related": "Connexe"
|
||||||
|
},
|
||||||
|
"copilot": {
|
||||||
|
"label": "Copilot"
|
||||||
|
},
|
||||||
|
"attach": {
|
||||||
|
"attachedFiles": "Fichiers joints",
|
||||||
|
"add": "Ajouter",
|
||||||
|
"clear": "Effacer",
|
||||||
|
"attach": "Joindre",
|
||||||
|
"uploading": "Téléversement...",
|
||||||
|
"files": "{count} fichiers"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"button": "Focus",
|
||||||
|
"modes": {
|
||||||
|
"webSearch": {
|
||||||
|
"title": "Tous",
|
||||||
|
"description": "Recherche sur tout Internet"
|
||||||
|
},
|
||||||
|
"academicSearch": {
|
||||||
|
"title": "Académique",
|
||||||
|
"description": "Recherche dans des articles académiques publiés"
|
||||||
|
},
|
||||||
|
"writingAssistant": {
|
||||||
|
"title": "Rédaction",
|
||||||
|
"description": "Discuter sans rechercher sur le web"
|
||||||
|
},
|
||||||
|
"wolframAlphaSearch": {
|
||||||
|
"title": "Wolfram Alpha",
|
||||||
|
"description": "Moteur de connaissances calculatoires"
|
||||||
|
},
|
||||||
|
"youtubeSearch": {
|
||||||
|
"title": "YouTube",
|
||||||
|
"description": "Rechercher et regarder des vidéos"
|
||||||
|
},
|
||||||
|
"redditSearch": {
|
||||||
|
"title": "Reddit",
|
||||||
|
"description": "Rechercher des discussions et des opinions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"modes": {
|
||||||
|
"speed": {
|
||||||
|
"title": "Vitesse",
|
||||||
|
"description": "Prioriser la vitesse pour obtenir la réponse la plus rapide"
|
||||||
|
},
|
||||||
|
"balanced": {
|
||||||
|
"title": "Équilibré",
|
||||||
|
"description": "Trouver un équilibre entre vitesse et précision"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"title": "Qualité (bientôt)",
|
||||||
|
"description": "Obtenir la réponse la plus complète et la plus précise"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageActions": {
|
||||||
|
"rewrite": "Réécrire"
|
||||||
|
},
|
||||||
|
"searchImages": {
|
||||||
|
"searchButton": "Rechercher des images"
|
||||||
|
},
|
||||||
|
"searchVideos": {
|
||||||
|
"searchButton": "Rechercher des vidéos",
|
||||||
|
"badge": "Vidéo"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"humidity": "Humidité",
|
||||||
|
"now": "Maintenant"
|
||||||
|
},
|
||||||
|
"newsArticleWidget": {
|
||||||
|
"error": "Impossible de charger les nouvelles."
|
||||||
|
},
|
||||||
|
"emptyChat": {
|
||||||
|
"title": "La recherche commence ici."
|
||||||
|
},
|
||||||
|
"emptyChatMessageInput": {
|
||||||
|
"placeholder": "Demandez-moi n'importe quoi..."
|
||||||
|
},
|
||||||
|
"deleteChat": {
|
||||||
|
"title": "Confirmation de suppression",
|
||||||
|
"description": "Voulez-vous vraiment supprimer cette conversation?",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"delete": "Supprimer"
|
||||||
|
},
|
||||||
|
"themeSwitcher": {
|
||||||
|
"options": {
|
||||||
|
"light": "Clair",
|
||||||
|
"dark": "Sombre"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
messages/fr-FR.json
Normal file
251
messages/fr-FR.json
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "Perplexica - Discuter avec Internet",
|
||||||
|
"description": "Perplexica est un chatbot IA connecté à Internet."
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "Perplexica - Discuter avec Internet",
|
||||||
|
"shortName": "Perplexica",
|
||||||
|
"description": "Perplexica est un chatbot IA connecté à Internet."
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "Accueil",
|
||||||
|
"discover": "Découvrir",
|
||||||
|
"library": "Bibliothèque",
|
||||||
|
"settings": "Paramètres"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"appName": "Perplexica",
|
||||||
|
"exportedOn": "Exporté le :",
|
||||||
|
"citations": "Citations :",
|
||||||
|
"user": "Utilisateur",
|
||||||
|
"assistant": "Assistant",
|
||||||
|
"errors": {
|
||||||
|
"noChatModelsAvailable": "Aucun modèle de chat disponible",
|
||||||
|
"chatProviderNotConfigured": "Aucun fournisseur de modèle de chat n'est configuré. Merci de le configurer depuis la page des paramètres ou le fichier de configuration.",
|
||||||
|
"noEmbeddingModelsAvailable": "Aucun modèle d'embedding disponible",
|
||||||
|
"cannotSendBeforeConfigReady": "Impossible d'envoyer un message avant la fin de la configuration",
|
||||||
|
"failedToDeleteChat": "Échec de la suppression du chat"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"exportAsMarkdown": "Exporter en Markdown",
|
||||||
|
"exportAsPDF": "Exporter en PDF"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"chatExportTitle": "Export de conversation : {title}"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"conditions": {
|
||||||
|
"clear": "Dégagé",
|
||||||
|
"mainlyClear": "Plutôt dégagé",
|
||||||
|
"partlyCloudy": "Partiellement nuageux",
|
||||||
|
"cloudy": "Nuageux",
|
||||||
|
"fog": "Brouillard",
|
||||||
|
"lightDrizzle": "Bruine faible",
|
||||||
|
"moderateDrizzle": "Bruine modérée",
|
||||||
|
"denseDrizzle": "Bruine forte",
|
||||||
|
"lightFreezingDrizzle": "Bruine verglaçante faible",
|
||||||
|
"denseFreezingDrizzle": "Bruine verglaçante forte",
|
||||||
|
"slightRain": "Pluie faible",
|
||||||
|
"moderateRain": "Pluie modérée",
|
||||||
|
"heavyRain": "Pluie forte",
|
||||||
|
"lightFreezingRain": "Pluie verglaçante faible",
|
||||||
|
"heavyFreezingRain": "Pluie verglaçante forte",
|
||||||
|
"slightSnowFall": "Neige faible",
|
||||||
|
"moderateSnowFall": "Neige modérée",
|
||||||
|
"heavySnowFall": "Fortes chutes de neige",
|
||||||
|
"snow": "Chute de neige",
|
||||||
|
"slightRainShowers": "Averses faibles",
|
||||||
|
"moderateRainShowers": "Averses",
|
||||||
|
"heavyRainShowers": "Fortes averses",
|
||||||
|
"slightSnowShowers": "Averses de neige faibles",
|
||||||
|
"moderateSnowShowers": "Averses de neige",
|
||||||
|
"heavySnowShowers": "Fortes averses de neige",
|
||||||
|
"thunderstorm": "Orage",
|
||||||
|
"thunderstormSlightHail": "Orage avec petites grêles",
|
||||||
|
"thunderstormHeavyHail": "Orage avec fortes grêles"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"home": {
|
||||||
|
"title": "Chat - Perplexica",
|
||||||
|
"description": "Discutez avec Internet, discutez avec Perplexica."
|
||||||
|
},
|
||||||
|
"discover": {
|
||||||
|
"title": "Découvrir",
|
||||||
|
"topics": {
|
||||||
|
"tech": "Tech et science",
|
||||||
|
"finance": "Finance",
|
||||||
|
"art": "Art et culture",
|
||||||
|
"sports": "Sports",
|
||||||
|
"entertainment": "Divertissement"
|
||||||
|
},
|
||||||
|
"errorFetchingData": "Erreur lors de la récupération des données"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"title": "Bibliothèque",
|
||||||
|
"empty": "Aucune conversation trouvée.",
|
||||||
|
"ago": "Il y a {time}"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Paramètres",
|
||||||
|
"sections": {
|
||||||
|
"preferences": "Préférences",
|
||||||
|
"automaticSearch": "Recherche automatique",
|
||||||
|
"systemInstructions": "Instructions système",
|
||||||
|
"modelSettings": "Paramètres du modèle",
|
||||||
|
"apiKeys": "Clés API"
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"theme": "Thème",
|
||||||
|
"measurementUnits": "Unités de mesure",
|
||||||
|
"language": "Langue",
|
||||||
|
"metric": "Métrique",
|
||||||
|
"imperial": "Impérial"
|
||||||
|
},
|
||||||
|
"automaticSearch": {
|
||||||
|
"image": {
|
||||||
|
"title": "Recherche d'images automatique",
|
||||||
|
"desc": "Rechercher automatiquement des images pertinentes dans les réponses du chat"
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"title": "Recherche de vidéos automatique",
|
||||||
|
"desc": "Rechercher automatiquement des vidéos pertinentes dans les réponses du chat"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"chatProvider": "Fournisseur du modèle de chat",
|
||||||
|
"chat": "Modèle de chat",
|
||||||
|
"noModels": "Aucun modèle disponible",
|
||||||
|
"invalidProvider": "Fournisseur invalide, veuillez vérifier les logs backend",
|
||||||
|
"custom": {
|
||||||
|
"modelName": "Nom du modèle",
|
||||||
|
"apiKey": "Clé API OpenAI personnalisée",
|
||||||
|
"baseUrl": "URL de base OpenAI personnalisée"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"embedding": {
|
||||||
|
"provider": "Fournisseur du modèle d'embedding",
|
||||||
|
"model": "Modèle d'embedding"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"openaiApiKey": "Clé API OpenAI",
|
||||||
|
"ollamaApiUrl": "URL de l'API Ollama",
|
||||||
|
"groqApiKey": "Clé API GROQ",
|
||||||
|
"anthropicApiKey": "Clé API Anthropic",
|
||||||
|
"geminiApiKey": "Clé API Gemini",
|
||||||
|
"deepseekApiKey": "Clé API Deepseek",
|
||||||
|
"aimlApiKey": "Clé API AI/ML",
|
||||||
|
"lmStudioApiUrl": "URL de l'API LM Studio"
|
||||||
|
},
|
||||||
|
"systemInstructions": {
|
||||||
|
"placeholder": "Toute instruction spécifique pour le LLM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"common": {
|
||||||
|
"viewMore": "Voir {count} de plus"
|
||||||
|
},
|
||||||
|
"messageInput": {
|
||||||
|
"placeholder": "Poser une question de suivi"
|
||||||
|
},
|
||||||
|
"messageBox": {
|
||||||
|
"sources": "Sources",
|
||||||
|
"answer": "Réponse",
|
||||||
|
"related": "Connexe"
|
||||||
|
},
|
||||||
|
"copilot": {
|
||||||
|
"label": "Copilot"
|
||||||
|
},
|
||||||
|
"attach": {
|
||||||
|
"attachedFiles": "Fichiers joints",
|
||||||
|
"add": "Ajouter",
|
||||||
|
"clear": "Effacer",
|
||||||
|
"attach": "Joindre",
|
||||||
|
"uploading": "Téléversement...",
|
||||||
|
"files": "{count} fichiers"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"button": "Focus",
|
||||||
|
"modes": {
|
||||||
|
"webSearch": {
|
||||||
|
"title": "Tous",
|
||||||
|
"description": "Recherche sur tout Internet"
|
||||||
|
},
|
||||||
|
"academicSearch": {
|
||||||
|
"title": "Académique",
|
||||||
|
"description": "Recherche dans des articles académiques publiés"
|
||||||
|
},
|
||||||
|
"writingAssistant": {
|
||||||
|
"title": "Rédaction",
|
||||||
|
"description": "Discuter sans rechercher sur le web"
|
||||||
|
},
|
||||||
|
"wolframAlphaSearch": {
|
||||||
|
"title": "Wolfram Alpha",
|
||||||
|
"description": "Moteur de connaissance computationnelle"
|
||||||
|
},
|
||||||
|
"youtubeSearch": {
|
||||||
|
"title": "YouTube",
|
||||||
|
"description": "Rechercher et regarder des vidéos"
|
||||||
|
},
|
||||||
|
"redditSearch": {
|
||||||
|
"title": "Reddit",
|
||||||
|
"description": "Rechercher des discussions et des opinions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"modes": {
|
||||||
|
"speed": {
|
||||||
|
"title": "Vitesse",
|
||||||
|
"description": "Prioriser la vitesse pour obtenir la réponse la plus rapide"
|
||||||
|
},
|
||||||
|
"balanced": {
|
||||||
|
"title": "Équilibré",
|
||||||
|
"description": "Trouver un équilibre entre vitesse et précision"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"title": "Qualité (bientôt)",
|
||||||
|
"description": "Obtenir la réponse la plus complète et la plus précise"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageActions": {
|
||||||
|
"rewrite": "Réécrire"
|
||||||
|
},
|
||||||
|
"searchImages": {
|
||||||
|
"searchButton": "Rechercher des images"
|
||||||
|
},
|
||||||
|
"searchVideos": {
|
||||||
|
"searchButton": "Rechercher des vidéos",
|
||||||
|
"badge": "Vidéo"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"humidity": "Humidité",
|
||||||
|
"now": "Maintenant"
|
||||||
|
},
|
||||||
|
"newsArticleWidget": {
|
||||||
|
"error": "Impossible de charger les actualités."
|
||||||
|
},
|
||||||
|
"emptyChat": {
|
||||||
|
"title": "La recherche commence ici."
|
||||||
|
},
|
||||||
|
"emptyChatMessageInput": {
|
||||||
|
"placeholder": "Demandez-moi n'importe quoi..."
|
||||||
|
},
|
||||||
|
"deleteChat": {
|
||||||
|
"title": "Confirmation de suppression",
|
||||||
|
"description": "Êtes-vous sûr de vouloir supprimer cette conversation ?",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"delete": "Supprimer"
|
||||||
|
},
|
||||||
|
"themeSwitcher": {
|
||||||
|
"options": {
|
||||||
|
"light": "Clair",
|
||||||
|
"dark": "Sombre"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
messages/ja.json
Normal file
251
messages/ja.json
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "Perplexica - インターネットと会話",
|
||||||
|
"description": "Perplexica はインターネットに接続された AI チャットボットです。"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "Perplexica - インターネットと会話",
|
||||||
|
"shortName": "Perplexica",
|
||||||
|
"description": "Perplexica はインターネットに接続された AI チャットボットです。"
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "ホーム",
|
||||||
|
"discover": "ディスカバー",
|
||||||
|
"library": "ライブラリ",
|
||||||
|
"settings": "設定"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"appName": "Perplexica",
|
||||||
|
"exportedOn": "書き出し日:",
|
||||||
|
"citations": "参考文献:",
|
||||||
|
"user": "ユーザー",
|
||||||
|
"assistant": "アシスタント",
|
||||||
|
"errors": {
|
||||||
|
"noChatModelsAvailable": "利用可能なチャットモデルがありません",
|
||||||
|
"chatProviderNotConfigured": "チャットモデルのプロバイダーが設定されていません。設定ページまたは設定ファイルで設定してください。",
|
||||||
|
"noEmbeddingModelsAvailable": "利用可能な埋め込みモデルがありません",
|
||||||
|
"cannotSendBeforeConfigReady": "設定が完了するまでメッセージを送信できません",
|
||||||
|
"failedToDeleteChat": "チャットの削除に失敗しました"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"exportAsMarkdown": "Markdown として書き出し",
|
||||||
|
"exportAsPDF": "PDF として書き出し"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"chatExportTitle": "チャット書き出し:{title}"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"conditions": {
|
||||||
|
"clear": "快晴",
|
||||||
|
"mainlyClear": "晴れ時々薄曇り",
|
||||||
|
"partlyCloudy": "所により曇り",
|
||||||
|
"cloudy": "曇り",
|
||||||
|
"fog": "霧",
|
||||||
|
"lightDrizzle": "弱い霧雨",
|
||||||
|
"moderateDrizzle": "やや強い霧雨",
|
||||||
|
"denseDrizzle": "強い霧雨",
|
||||||
|
"lightFreezingDrizzle": "弱い着氷性霧雨",
|
||||||
|
"denseFreezingDrizzle": "強い着氷性霧雨",
|
||||||
|
"slightRain": "小雨",
|
||||||
|
"moderateRain": "雨",
|
||||||
|
"heavyRain": "大雨",
|
||||||
|
"lightFreezingRain": "弱い凍雨",
|
||||||
|
"heavyFreezingRain": "強い凍雨",
|
||||||
|
"slightSnowFall": "小雪",
|
||||||
|
"moderateSnowFall": "雪",
|
||||||
|
"heavySnowFall": "大雪",
|
||||||
|
"snow": "降雪",
|
||||||
|
"slightRainShowers": "にわか小雨",
|
||||||
|
"moderateRainShowers": "にわか雨",
|
||||||
|
"heavyRainShowers": "激しいにわか雨",
|
||||||
|
"slightSnowShowers": "にわか小雪",
|
||||||
|
"moderateSnowShowers": "にわか雪",
|
||||||
|
"heavySnowShowers": "激しいにわか雪",
|
||||||
|
"thunderstorm": "雷雨",
|
||||||
|
"thunderstormSlightHail": "雷雨(小さな雹)",
|
||||||
|
"thunderstormHeavyHail": "雷雨(大きな雹)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"home": {
|
||||||
|
"title": "チャット - Perplexica",
|
||||||
|
"description": "インターネットと会話し、Perplexica と対話しましょう。"
|
||||||
|
},
|
||||||
|
"discover": {
|
||||||
|
"title": "ディスカバー",
|
||||||
|
"topics": {
|
||||||
|
"tech": "テック・サイエンス",
|
||||||
|
"finance": "ファイナンス",
|
||||||
|
"art": "アート・文化",
|
||||||
|
"sports": "スポーツ",
|
||||||
|
"entertainment": "エンタメ"
|
||||||
|
},
|
||||||
|
"errorFetchingData": "データの取得中にエラーが発生しました"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"title": "ライブラリ",
|
||||||
|
"empty": "チャットが見つかりません。",
|
||||||
|
"ago": "{time} 前"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "設定",
|
||||||
|
"sections": {
|
||||||
|
"preferences": "環境設定",
|
||||||
|
"automaticSearch": "自動検索",
|
||||||
|
"systemInstructions": "システム指示",
|
||||||
|
"modelSettings": "モデル設定",
|
||||||
|
"apiKeys": "API キー"
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"theme": "テーマ",
|
||||||
|
"measurementUnits": "単位",
|
||||||
|
"language": "言語",
|
||||||
|
"metric": "メートル法",
|
||||||
|
"imperial": "ヤード・ポンド法"
|
||||||
|
},
|
||||||
|
"automaticSearch": {
|
||||||
|
"image": {
|
||||||
|
"title": "自動画像検索",
|
||||||
|
"desc": "チャットの回答で関連画像を自動検索"
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"title": "自動画像検索",
|
||||||
|
"desc": "チャットの回答で関連動画を自動検索"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"chatProvider": "チャットモデルのプロバイダー",
|
||||||
|
"chat": "チャットモデル",
|
||||||
|
"noModels": "利用可能なモデルはありません",
|
||||||
|
"invalidProvider": "無効なプロバイダーです。バックエンドのログを確認してください",
|
||||||
|
"custom": {
|
||||||
|
"modelName": "モデル名",
|
||||||
|
"apiKey": "カスタム OpenAI API キー",
|
||||||
|
"baseUrl": "カスタム OpenAI Base URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"embedding": {
|
||||||
|
"provider": "埋め込みモデルのプロバイダー",
|
||||||
|
"model": "埋め込みモデル"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"openaiApiKey": "OpenAI API キー",
|
||||||
|
"ollamaApiUrl": "Ollama API URL",
|
||||||
|
"groqApiKey": "GROQ API キー",
|
||||||
|
"anthropicApiKey": "Anthropic API キー",
|
||||||
|
"geminiApiKey": "Gemini API キー",
|
||||||
|
"deepseekApiKey": "Deepseek API キー",
|
||||||
|
"aimlApiKey": "AI/ML API キー",
|
||||||
|
"lmStudioApiUrl": "LM Studio API URL"
|
||||||
|
},
|
||||||
|
"systemInstructions": {
|
||||||
|
"placeholder": "LLM への特別な指示があれば入力してください"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"common": {
|
||||||
|
"viewMore": "さらに {count} 件を表示"
|
||||||
|
},
|
||||||
|
"messageInput": {
|
||||||
|
"placeholder": "追質問する"
|
||||||
|
},
|
||||||
|
"messageBox": {
|
||||||
|
"sources": "出典",
|
||||||
|
"answer": "回答",
|
||||||
|
"related": "関連"
|
||||||
|
},
|
||||||
|
"copilot": {
|
||||||
|
"label": "Copilot"
|
||||||
|
},
|
||||||
|
"attach": {
|
||||||
|
"attachedFiles": "添付ファイル",
|
||||||
|
"add": "追加",
|
||||||
|
"clear": "クリア",
|
||||||
|
"attach": "添付",
|
||||||
|
"uploading": "アップロード中...",
|
||||||
|
"files": "{count} 件のファイル"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"button": "フォーカス",
|
||||||
|
"modes": {
|
||||||
|
"webSearch": {
|
||||||
|
"title": "すべて",
|
||||||
|
"description": "インターネット全体を検索"
|
||||||
|
},
|
||||||
|
"academicSearch": {
|
||||||
|
"title": "学術",
|
||||||
|
"description": "公開済みの学術論文を検索"
|
||||||
|
},
|
||||||
|
"writingAssistant": {
|
||||||
|
"title": "ライティング",
|
||||||
|
"description": "ウェブ検索をせずにチャット"
|
||||||
|
},
|
||||||
|
"wolframAlphaSearch": {
|
||||||
|
"title": "Wolfram Alpha",
|
||||||
|
"description": "計算知識エンジン"
|
||||||
|
},
|
||||||
|
"youtubeSearch": {
|
||||||
|
"title": "YouTube",
|
||||||
|
"description": "動画を検索・視聴"
|
||||||
|
},
|
||||||
|
"redditSearch": {
|
||||||
|
"title": "Reddit",
|
||||||
|
"description": "議論と意見を検索"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"modes": {
|
||||||
|
"speed": {
|
||||||
|
"title": "スピード",
|
||||||
|
"description": "速度を優先し、最速で回答を取得"
|
||||||
|
},
|
||||||
|
"balanced": {
|
||||||
|
"title": "バランス",
|
||||||
|
"description": "速度と正確性のバランスを取る"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"title": "クオリティ(近日対応)",
|
||||||
|
"description": "最も丁寧で正確な回答を取得"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageActions": {
|
||||||
|
"rewrite": "書き直す"
|
||||||
|
},
|
||||||
|
"searchImages": {
|
||||||
|
"searchButton": "画像を検索"
|
||||||
|
},
|
||||||
|
"searchVideos": {
|
||||||
|
"searchButton": "動画を検索",
|
||||||
|
"badge": "動画"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"humidity": "湿度",
|
||||||
|
"now": "現在"
|
||||||
|
},
|
||||||
|
"newsArticleWidget": {
|
||||||
|
"error": "ニュースを読み込めませんでした。"
|
||||||
|
},
|
||||||
|
"emptyChat": {
|
||||||
|
"title": "ここからリサーチが始まる。"
|
||||||
|
},
|
||||||
|
"emptyChatMessageInput": {
|
||||||
|
"placeholder": "何でも聞いてください..."
|
||||||
|
},
|
||||||
|
"deleteChat": {
|
||||||
|
"title": "削除の確認",
|
||||||
|
"description": "このチャットを削除してよろしいですか?",
|
||||||
|
"cancel": "キャンセル",
|
||||||
|
"delete": "削除"
|
||||||
|
},
|
||||||
|
"themeSwitcher": {
|
||||||
|
"options": {
|
||||||
|
"light": "ライト",
|
||||||
|
"dark": "ダーク"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
messages/ko.json
Normal file
251
messages/ko.json
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "Perplexica - 인터넷과 대화",
|
||||||
|
"description": "Perplexica는 인터넷에 연결된 AI 챗봇입니다."
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "Perplexica - 인터넷과 대화",
|
||||||
|
"shortName": "Perplexica",
|
||||||
|
"description": "Perplexica는 인터넷에 연결된 AI 챗봇입니다."
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "홈",
|
||||||
|
"discover": "디스커버",
|
||||||
|
"library": "라이브러리",
|
||||||
|
"settings": "설정"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"appName": "Perplexica",
|
||||||
|
"exportedOn": "내보낸 날짜:",
|
||||||
|
"citations": "참고 출처:",
|
||||||
|
"user": "사용자",
|
||||||
|
"assistant": "도우미",
|
||||||
|
"errors": {
|
||||||
|
"noChatModelsAvailable": "사용 가능한 채팅 모델이 없습니다",
|
||||||
|
"chatProviderNotConfigured": "채팅 모델 공급자가 구성되지 않은 것 같습니다. 설정 페이지 또는 구성 파일에서 설정하세요.",
|
||||||
|
"noEmbeddingModelsAvailable": "사용 가능한 임베딩 모델이 없습니다",
|
||||||
|
"cannotSendBeforeConfigReady": "구성이 완료되기 전에는 메시지를 보낼 수 없습니다",
|
||||||
|
"failedToDeleteChat": "채팅 삭제에 실패했습니다"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"exportAsMarkdown": "Markdown으로 내보내기",
|
||||||
|
"exportAsPDF": "PDF로 내보내기"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"chatExportTitle": "채팅 내보내기: {title}"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"conditions": {
|
||||||
|
"clear": "맑음",
|
||||||
|
"mainlyClear": "대체로 맑음",
|
||||||
|
"partlyCloudy": "부분적으로 구름",
|
||||||
|
"cloudy": "흐림",
|
||||||
|
"fog": "안개",
|
||||||
|
"lightDrizzle": "약한 이슬비",
|
||||||
|
"moderateDrizzle": "보통 이슬비",
|
||||||
|
"denseDrizzle": "강한 이슬비",
|
||||||
|
"lightFreezingDrizzle": "약한 착빙성 이슬비",
|
||||||
|
"denseFreezingDrizzle": "강한 착빙성 이슬비",
|
||||||
|
"slightRain": "약한 비",
|
||||||
|
"moderateRain": "보통 비",
|
||||||
|
"heavyRain": "강한 비",
|
||||||
|
"lightFreezingRain": "약한 어는 비",
|
||||||
|
"heavyFreezingRain": "강한 어는 비",
|
||||||
|
"slightSnowFall": "약한 눈",
|
||||||
|
"moderateSnowFall": "눈",
|
||||||
|
"heavySnowFall": "폭설",
|
||||||
|
"snow": "강설",
|
||||||
|
"slightRainShowers": "약한 소나기",
|
||||||
|
"moderateRainShowers": "소나기",
|
||||||
|
"heavyRainShowers": "강한 소나기",
|
||||||
|
"slightSnowShowers": "약한 눈보라",
|
||||||
|
"moderateSnowShowers": "눈보라",
|
||||||
|
"heavySnowShowers": "강한 눈보라",
|
||||||
|
"thunderstorm": "뇌우",
|
||||||
|
"thunderstormSlightHail": "약한 우박을 동반한 뇌우",
|
||||||
|
"thunderstormHeavyHail": "강한 우박을 동반한 뇌우"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"home": {
|
||||||
|
"title": "채팅 - Perplexica",
|
||||||
|
"description": "인터넷과 대화하고 Perplexica와 대화하세요."
|
||||||
|
},
|
||||||
|
"discover": {
|
||||||
|
"title": "디스커버",
|
||||||
|
"topics": {
|
||||||
|
"tech": "기술과 과학",
|
||||||
|
"finance": "금융",
|
||||||
|
"art": "예술과 문화",
|
||||||
|
"sports": "스포츠",
|
||||||
|
"entertainment": "엔터테인먼트"
|
||||||
|
},
|
||||||
|
"errorFetchingData": "데이터를 가져오는 중 오류가 발생했습니다"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"title": "라이브러리",
|
||||||
|
"empty": "채팅을 찾을 수 없습니다.",
|
||||||
|
"ago": "{time} 전"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "설정",
|
||||||
|
"sections": {
|
||||||
|
"preferences": "기본 설정",
|
||||||
|
"automaticSearch": "자동 검색",
|
||||||
|
"systemInstructions": "시스템 지시",
|
||||||
|
"modelSettings": "모델 설정",
|
||||||
|
"apiKeys": "API 키"
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"theme": "테마",
|
||||||
|
"measurementUnits": "단위",
|
||||||
|
"language": "언어",
|
||||||
|
"metric": "미터법",
|
||||||
|
"imperial": "야드파운드법"
|
||||||
|
},
|
||||||
|
"automaticSearch": {
|
||||||
|
"image": {
|
||||||
|
"title": "자동 이미지 검색",
|
||||||
|
"desc": "채팅 응답에서 관련 이미지를 자동으로 검색"
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"title": "자동 비디오 검색",
|
||||||
|
"desc": "채팅 응답에서 관련 비디오를 자동으로 검색"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"chatProvider": "채팅 모델 공급자",
|
||||||
|
"chat": "채팅 모델",
|
||||||
|
"noModels": "사용 가능한 모델이 없습니다",
|
||||||
|
"invalidProvider": "잘못된 공급자입니다. 백엔드 로그를 확인하세요",
|
||||||
|
"custom": {
|
||||||
|
"modelName": "모델 이름",
|
||||||
|
"apiKey": "사용자 지정 OpenAI API 키",
|
||||||
|
"baseUrl": "사용자 지정 OpenAI Base URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"embedding": {
|
||||||
|
"provider": "임베딩 모델 공급자",
|
||||||
|
"model": "임베딩 모델"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"openaiApiKey": "OpenAI API 키",
|
||||||
|
"ollamaApiUrl": "Ollama API URL",
|
||||||
|
"groqApiKey": "GROQ API 키",
|
||||||
|
"anthropicApiKey": "Anthropic API 키",
|
||||||
|
"geminiApiKey": "Gemini API 키",
|
||||||
|
"deepseekApiKey": "Deepseek API 키",
|
||||||
|
"aimlApiKey": "AI/ML API 키",
|
||||||
|
"lmStudioApiUrl": "LM Studio API URL"
|
||||||
|
},
|
||||||
|
"systemInstructions": {
|
||||||
|
"placeholder": "LLM에 대한 특별한 지시사항"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"common": {
|
||||||
|
"viewMore": "추가 {count}개 보기"
|
||||||
|
},
|
||||||
|
"messageInput": {
|
||||||
|
"placeholder": "후속 질문하기"
|
||||||
|
},
|
||||||
|
"messageBox": {
|
||||||
|
"sources": "출처",
|
||||||
|
"answer": "답변",
|
||||||
|
"related": "관련"
|
||||||
|
},
|
||||||
|
"copilot": {
|
||||||
|
"label": "Copilot"
|
||||||
|
},
|
||||||
|
"attach": {
|
||||||
|
"attachedFiles": "첨부된 파일",
|
||||||
|
"add": "추가",
|
||||||
|
"clear": "지우기",
|
||||||
|
"attach": "첨부",
|
||||||
|
"uploading": "업로드 중...",
|
||||||
|
"files": "{count}개 파일"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"button": "포커스",
|
||||||
|
"modes": {
|
||||||
|
"webSearch": {
|
||||||
|
"title": "전체",
|
||||||
|
"description": "인터넷 전체 검색"
|
||||||
|
},
|
||||||
|
"academicSearch": {
|
||||||
|
"title": "학술",
|
||||||
|
"description": "발표된 학술 논문 검색"
|
||||||
|
},
|
||||||
|
"writingAssistant": {
|
||||||
|
"title": "글쓰기",
|
||||||
|
"description": "웹을 검색하지 않고 채팅"
|
||||||
|
},
|
||||||
|
"wolframAlphaSearch": {
|
||||||
|
"title": "Wolfram Alpha",
|
||||||
|
"description": "계산 지식 엔진"
|
||||||
|
},
|
||||||
|
"youtubeSearch": {
|
||||||
|
"title": "YouTube",
|
||||||
|
"description": "동영상 검색 및 시청"
|
||||||
|
},
|
||||||
|
"redditSearch": {
|
||||||
|
"title": "Reddit",
|
||||||
|
"description": "토론과 의견 검색"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"modes": {
|
||||||
|
"speed": {
|
||||||
|
"title": "속도",
|
||||||
|
"description": "속도를 우선하여 가장 빠른 답변 얻기"
|
||||||
|
},
|
||||||
|
"balanced": {
|
||||||
|
"title": "균형",
|
||||||
|
"description": "속도와 정확성의 균형 맞추기"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"title": "품질(곧 제공)",
|
||||||
|
"description": "가장 철저하고 정확한 답변 얻기"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageActions": {
|
||||||
|
"rewrite": "다시 쓰기"
|
||||||
|
},
|
||||||
|
"searchImages": {
|
||||||
|
"searchButton": "이미지 검색"
|
||||||
|
},
|
||||||
|
"searchVideos": {
|
||||||
|
"searchButton": "동영상 검색",
|
||||||
|
"badge": "동영상"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"humidity": "습도",
|
||||||
|
"now": "지금"
|
||||||
|
},
|
||||||
|
"newsArticleWidget": {
|
||||||
|
"error": "뉴스를 불러올 수 없습니다."
|
||||||
|
},
|
||||||
|
"emptyChat": {
|
||||||
|
"title": "연구는 여기에서 시작됩니다."
|
||||||
|
},
|
||||||
|
"emptyChatMessageInput": {
|
||||||
|
"placeholder": "아무거나 물어보세요..."
|
||||||
|
},
|
||||||
|
"deleteChat": {
|
||||||
|
"title": "삭제 확인",
|
||||||
|
"description": "이 채팅을 삭제하시겠습니까?",
|
||||||
|
"cancel": "취소",
|
||||||
|
"delete": "삭제"
|
||||||
|
},
|
||||||
|
"themeSwitcher": {
|
||||||
|
"options": {
|
||||||
|
"light": "라이트",
|
||||||
|
"dark": "다크"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
messages/zh-CN.json
Normal file
251
messages/zh-CN.json
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "Perplexica - 与网络对话",
|
||||||
|
"description": "Perplexica 是一个能连接互联网的 AI 聊天机器人。"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "Perplexica - 与网络对话",
|
||||||
|
"shortName": "Perplexica",
|
||||||
|
"description": "Perplexica 是一个能连接互联网的 AI 聊天机器人。"
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "首页",
|
||||||
|
"discover": "探索",
|
||||||
|
"library": "资料库",
|
||||||
|
"settings": "设置"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"appName": "Perplexica",
|
||||||
|
"exportedOn": "导出日期:",
|
||||||
|
"citations": "参考来源:",
|
||||||
|
"user": "使用者",
|
||||||
|
"assistant": "助理",
|
||||||
|
"errors": {
|
||||||
|
"noChatModelsAvailable": "没有可用的聊天模型",
|
||||||
|
"chatProviderNotConfigured": "看起来你还没有配置任何聊天模型供应商,请到设置页或配置文件进行设置。",
|
||||||
|
"noEmbeddingModelsAvailable": "没有可用的向量嵌入模型",
|
||||||
|
"cannotSendBeforeConfigReady": "在设置就绪前无法发送消息",
|
||||||
|
"failedToDeleteChat": "删除聊天失败"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"exportAsMarkdown": "导出为 Markdown",
|
||||||
|
"exportAsPDF": "导出为 PDF"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"chatExportTitle": "聊天导出:{title}"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"conditions": {
|
||||||
|
"clear": "晴朗",
|
||||||
|
"mainlyClear": "大致晴朗",
|
||||||
|
"partlyCloudy": "局部多云",
|
||||||
|
"cloudy": "多云",
|
||||||
|
"fog": "雾",
|
||||||
|
"lightDrizzle": "小毛毛雨",
|
||||||
|
"moderateDrizzle": "中等毛毛雨",
|
||||||
|
"denseDrizzle": "大毛毛雨",
|
||||||
|
"lightFreezingDrizzle": "轻微冰雾雨",
|
||||||
|
"denseFreezingDrizzle": "强冰雾雨",
|
||||||
|
"slightRain": "小雨",
|
||||||
|
"moderateRain": "中雨",
|
||||||
|
"heavyRain": "大雨",
|
||||||
|
"lightFreezingRain": "轻微冻雨",
|
||||||
|
"heavyFreezingRain": "强冻雨",
|
||||||
|
"slightSnowFall": "小雪",
|
||||||
|
"moderateSnowFall": "中雪",
|
||||||
|
"heavySnowFall": "大雪",
|
||||||
|
"snow": "降雪",
|
||||||
|
"slightRainShowers": "短暂小雨",
|
||||||
|
"moderateRainShowers": "短暂中雨",
|
||||||
|
"heavyRainShowers": "短暂大雨",
|
||||||
|
"slightSnowShowers": "短暂小雪",
|
||||||
|
"moderateSnowShowers": "短暂中雪",
|
||||||
|
"heavySnowShowers": "短暂大雪",
|
||||||
|
"thunderstorm": "雷雨",
|
||||||
|
"thunderstormSlightHail": "雷雨伴随小冰雹",
|
||||||
|
"thunderstormHeavyHail": "雷雨伴随大冰雹"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"home": {
|
||||||
|
"title": "聊天 - Perplexica",
|
||||||
|
"description": "连上互联网聊聊天,与 Perplexica 对话。"
|
||||||
|
},
|
||||||
|
"discover": {
|
||||||
|
"title": "探索",
|
||||||
|
"topics": {
|
||||||
|
"tech": "科技与科学",
|
||||||
|
"finance": "财经",
|
||||||
|
"art": "艺术与文化",
|
||||||
|
"sports": "运动",
|
||||||
|
"entertainment": "娱乐"
|
||||||
|
},
|
||||||
|
"errorFetchingData": "获取数据时发生错误"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"title": "资料库",
|
||||||
|
"empty": "没有找到任何聊天记录。",
|
||||||
|
"ago": "{time} 前"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "设置",
|
||||||
|
"sections": {
|
||||||
|
"preferences": "偏好设置",
|
||||||
|
"automaticSearch": "自动搜索",
|
||||||
|
"systemInstructions": "系统指示",
|
||||||
|
"modelSettings": "模型设置",
|
||||||
|
"apiKeys": "API 密钥"
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"theme": "主题",
|
||||||
|
"measurementUnits": "计量单位",
|
||||||
|
"language": "语言",
|
||||||
|
"metric": "公制",
|
||||||
|
"imperial": "英制"
|
||||||
|
},
|
||||||
|
"automaticSearch": {
|
||||||
|
"image": {
|
||||||
|
"title": "自动图片搜索",
|
||||||
|
"desc": "在聊天回复中自动搜索相关图片"
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"title": "自动视频搜索",
|
||||||
|
"desc": "在聊天回复中自动搜索相关视频"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"chatProvider": "聊天模型供应商",
|
||||||
|
"chat": "聊天模型",
|
||||||
|
"noModels": "没有可用的模型",
|
||||||
|
"invalidProvider": "供应商无效,请检查后端日志",
|
||||||
|
"custom": {
|
||||||
|
"modelName": "模型名称",
|
||||||
|
"apiKey": "自定义 OpenAI API 密钥",
|
||||||
|
"baseUrl": "自定义 OpenAI Base URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"embedding": {
|
||||||
|
"provider": "向量嵌入供应商",
|
||||||
|
"model": "向量嵌入模型"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"openaiApiKey": "OpenAI API 密钥",
|
||||||
|
"ollamaApiUrl": "Ollama API 地址",
|
||||||
|
"groqApiKey": "GROQ API 密钥",
|
||||||
|
"anthropicApiKey": "Anthropic API 密钥",
|
||||||
|
"geminiApiKey": "Gemini API 密钥",
|
||||||
|
"deepseekApiKey": "Deepseek API 密钥",
|
||||||
|
"aimlApiKey": "AI/ML 密钥",
|
||||||
|
"lmStudioApiUrl": "LM Studio API 地址"
|
||||||
|
},
|
||||||
|
"systemInstructions": {
|
||||||
|
"placeholder": "任何要给 LLM 的特别指示"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"common": {
|
||||||
|
"viewMore": "查看另外 {count} 项"
|
||||||
|
},
|
||||||
|
"messageInput": {
|
||||||
|
"placeholder": "提出追问"
|
||||||
|
},
|
||||||
|
"messageBox": {
|
||||||
|
"sources": "参考来源",
|
||||||
|
"answer": "回答",
|
||||||
|
"related": "相关内容"
|
||||||
|
},
|
||||||
|
"copilot": {
|
||||||
|
"label": "Copilot"
|
||||||
|
},
|
||||||
|
"attach": {
|
||||||
|
"attachedFiles": "已附加的文件",
|
||||||
|
"add": "新增",
|
||||||
|
"clear": "清除",
|
||||||
|
"attach": "附加",
|
||||||
|
"uploading": "上传中...",
|
||||||
|
"files": "{count} 个文件"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"button": "焦点",
|
||||||
|
"modes": {
|
||||||
|
"webSearch": {
|
||||||
|
"title": "全部",
|
||||||
|
"description": "在整个互联网搜索"
|
||||||
|
},
|
||||||
|
"academicSearch": {
|
||||||
|
"title": "学术",
|
||||||
|
"description": "搜索已发表的学术论文"
|
||||||
|
},
|
||||||
|
"writingAssistant": {
|
||||||
|
"title": "写作",
|
||||||
|
"description": "不搜索网络,直接聊天"
|
||||||
|
},
|
||||||
|
"wolframAlphaSearch": {
|
||||||
|
"title": "Wolfram Alpha",
|
||||||
|
"description": "计算型知识引擎"
|
||||||
|
},
|
||||||
|
"youtubeSearch": {
|
||||||
|
"title": "YouTube",
|
||||||
|
"description": "搜索和观看视频"
|
||||||
|
},
|
||||||
|
"redditSearch": {
|
||||||
|
"title": "Reddit",
|
||||||
|
"description": "搜索讨论与观点"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"modes": {
|
||||||
|
"speed": {
|
||||||
|
"title": "速度",
|
||||||
|
"description": "优先速度,以最快的方式得到答案。"
|
||||||
|
},
|
||||||
|
"balanced": {
|
||||||
|
"title": "平衡",
|
||||||
|
"description": "在速度与准确度之间取得平衡"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"title": "品质(即将推出)",
|
||||||
|
"description": "取得最完整与最精确的回答"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageActions": {
|
||||||
|
"rewrite": "重写"
|
||||||
|
},
|
||||||
|
"searchImages": {
|
||||||
|
"searchButton": "搜索图片"
|
||||||
|
},
|
||||||
|
"searchVideos": {
|
||||||
|
"searchButton": "搜索视频",
|
||||||
|
"badge": "视频"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"humidity": "湿度",
|
||||||
|
"now": "现在"
|
||||||
|
},
|
||||||
|
"newsArticleWidget": {
|
||||||
|
"error": "无法载入新闻。"
|
||||||
|
},
|
||||||
|
"emptyChat": {
|
||||||
|
"title": "研究从这里开始。"
|
||||||
|
},
|
||||||
|
"emptyChatMessageInput": {
|
||||||
|
"placeholder": "问我任何事..."
|
||||||
|
},
|
||||||
|
"deleteChat": {
|
||||||
|
"title": "删除确认",
|
||||||
|
"description": "确定要删除此聊天吗?",
|
||||||
|
"cancel": "取消",
|
||||||
|
"delete": "删除"
|
||||||
|
},
|
||||||
|
"themeSwitcher": {
|
||||||
|
"options": {
|
||||||
|
"light": "浅色",
|
||||||
|
"dark": "深色"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
messages/zh-HK.json
Normal file
251
messages/zh-HK.json
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "Perplexica - 與網絡對話",
|
||||||
|
"description": "Perplexica 是一個能連接互聯網的 AI 聊天機械人。"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "Perplexica - 與網絡對話",
|
||||||
|
"shortName": "Perplexica",
|
||||||
|
"description": "Perplexica 是一個能連接互聯網的 AI 聊天機械人。"
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "主頁",
|
||||||
|
"discover": "探索",
|
||||||
|
"library": "資料庫",
|
||||||
|
"settings": "設定"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"appName": "Perplexica",
|
||||||
|
"exportedOn": "匯出日期:",
|
||||||
|
"citations": "參考來源:",
|
||||||
|
"user": "用戶",
|
||||||
|
"assistant": "助理",
|
||||||
|
"errors": {
|
||||||
|
"noChatModelsAvailable": "沒有可用的聊天模型",
|
||||||
|
"chatProviderNotConfigured": "你似乎未有設定任何聊天模型供應商,請到設定頁或設定檔進行設定。",
|
||||||
|
"noEmbeddingModelsAvailable": "沒有可用的向量嵌入模型",
|
||||||
|
"cannotSendBeforeConfigReady": "在設定完成前無法發送訊息",
|
||||||
|
"failedToDeleteChat": "刪除聊天失敗"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"exportAsMarkdown": "匯出為 Markdown",
|
||||||
|
"exportAsPDF": "匯出為 PDF"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"chatExportTitle": "聊天匯出:{title}"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"conditions": {
|
||||||
|
"clear": "天晴",
|
||||||
|
"mainlyClear": "大致天晴",
|
||||||
|
"partlyCloudy": "局部多雲",
|
||||||
|
"cloudy": "多雲",
|
||||||
|
"fog": "霧",
|
||||||
|
"lightDrizzle": "微毛毛雨",
|
||||||
|
"moderateDrizzle": "毛毛雨",
|
||||||
|
"denseDrizzle": "密集毛毛雨",
|
||||||
|
"lightFreezingDrizzle": "輕微凍霧雨",
|
||||||
|
"denseFreezingDrizzle": "強凍霧雨",
|
||||||
|
"slightRain": "微雨",
|
||||||
|
"moderateRain": "雨",
|
||||||
|
"heavyRain": "大雨",
|
||||||
|
"lightFreezingRain": "輕微凍雨",
|
||||||
|
"heavyFreezingRain": "強凍雨",
|
||||||
|
"slightSnowFall": "微雪",
|
||||||
|
"moderateSnowFall": "下雪",
|
||||||
|
"heavySnowFall": "大雪",
|
||||||
|
"snow": "降雪",
|
||||||
|
"slightRainShowers": "驟雨",
|
||||||
|
"moderateRainShowers": "驟雨",
|
||||||
|
"heavyRainShowers": "大驟雨",
|
||||||
|
"slightSnowShowers": "驟雪",
|
||||||
|
"moderateSnowShowers": "驟雪",
|
||||||
|
"heavySnowShowers": "大驟雪",
|
||||||
|
"thunderstorm": "雷暴",
|
||||||
|
"thunderstormSlightHail": "雷暴(小冰雹)",
|
||||||
|
"thunderstormHeavyHail": "雷暴(大冰雹)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"home": {
|
||||||
|
"title": "聊天 - Perplexica",
|
||||||
|
"description": "連接網絡傾下計,與 Perplexica 對話。"
|
||||||
|
},
|
||||||
|
"discover": {
|
||||||
|
"title": "探索",
|
||||||
|
"topics": {
|
||||||
|
"tech": "科技與科學",
|
||||||
|
"finance": "財經",
|
||||||
|
"art": "藝術與文化",
|
||||||
|
"sports": "體育",
|
||||||
|
"entertainment": "娛樂"
|
||||||
|
},
|
||||||
|
"errorFetchingData": "取得資料時發生錯誤"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"title": "資料庫",
|
||||||
|
"empty": "未找到任何聊天紀錄。",
|
||||||
|
"ago": "{time} 前"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "設定",
|
||||||
|
"sections": {
|
||||||
|
"preferences": "偏好設定",
|
||||||
|
"automaticSearch": "自動搜尋",
|
||||||
|
"systemInstructions": "系統指示",
|
||||||
|
"modelSettings": "模型設定",
|
||||||
|
"apiKeys": "API 金鑰"
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"theme": "主題",
|
||||||
|
"measurementUnits": "度量單位",
|
||||||
|
"language": "語言",
|
||||||
|
"metric": "公制",
|
||||||
|
"imperial": "英制"
|
||||||
|
},
|
||||||
|
"automaticSearch": {
|
||||||
|
"image": {
|
||||||
|
"title": "自動圖片搜尋",
|
||||||
|
"desc": "在聊天回應中自動搜尋相關圖片"
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"title": "自動影片搜尋",
|
||||||
|
"desc": "在聊天回應中自動搜尋相關影片"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"chatProvider": "聊天模型供應商",
|
||||||
|
"chat": "聊天模型",
|
||||||
|
"noModels": "沒有可用的模型",
|
||||||
|
"invalidProvider": "供應商無效,請檢查後端日誌",
|
||||||
|
"custom": {
|
||||||
|
"modelName": "模型名稱",
|
||||||
|
"apiKey": "自訂 OpenAI API 金鑰",
|
||||||
|
"baseUrl": "自訂 OpenAI Base URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"embedding": {
|
||||||
|
"provider": "向量嵌入供應商",
|
||||||
|
"model": "向量嵌入模型"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"openaiApiKey": "OpenAI API 金鑰",
|
||||||
|
"ollamaApiUrl": "Ollama API 位址",
|
||||||
|
"groqApiKey": "GROQ API 金鑰",
|
||||||
|
"anthropicApiKey": "Anthropic API 金鑰",
|
||||||
|
"geminiApiKey": "Gemini API 金鑰",
|
||||||
|
"deepseekApiKey": "Deepseek API 金鑰",
|
||||||
|
"aimlApiKey": "AI/ML 金鑰",
|
||||||
|
"lmStudioApiUrl": "LM Studio API 位址"
|
||||||
|
},
|
||||||
|
"systemInstructions": {
|
||||||
|
"placeholder": "任何要給 LLM 的特別指示"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"common": {
|
||||||
|
"viewMore": "查看另外 {count} 項"
|
||||||
|
},
|
||||||
|
"messageInput": {
|
||||||
|
"placeholder": "提出追問"
|
||||||
|
},
|
||||||
|
"messageBox": {
|
||||||
|
"sources": "參考來源",
|
||||||
|
"answer": "回答",
|
||||||
|
"related": "相關內容"
|
||||||
|
},
|
||||||
|
"copilot": {
|
||||||
|
"label": "Copilot"
|
||||||
|
},
|
||||||
|
"attach": {
|
||||||
|
"attachedFiles": "已附加的檔案",
|
||||||
|
"add": "新增",
|
||||||
|
"clear": "清除",
|
||||||
|
"attach": "附加",
|
||||||
|
"uploading": "上載中...",
|
||||||
|
"files": "{count} 個檔案"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"button": "焦點",
|
||||||
|
"modes": {
|
||||||
|
"webSearch": {
|
||||||
|
"title": "全部",
|
||||||
|
"description": "在整個網絡上搜尋"
|
||||||
|
},
|
||||||
|
"academicSearch": {
|
||||||
|
"title": "學術",
|
||||||
|
"description": "搜尋已發表的學術論文"
|
||||||
|
},
|
||||||
|
"writingAssistant": {
|
||||||
|
"title": "寫作",
|
||||||
|
"description": "不搜尋網絡,直接聊天"
|
||||||
|
},
|
||||||
|
"wolframAlphaSearch": {
|
||||||
|
"title": "Wolfram Alpha",
|
||||||
|
"description": "計算型知識引擎"
|
||||||
|
},
|
||||||
|
"youtubeSearch": {
|
||||||
|
"title": "YouTube",
|
||||||
|
"description": "搜尋及觀看影片"
|
||||||
|
},
|
||||||
|
"redditSearch": {
|
||||||
|
"title": "Reddit",
|
||||||
|
"description": "搜尋討論與觀點"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"modes": {
|
||||||
|
"speed": {
|
||||||
|
"title": "速度",
|
||||||
|
"description": "優先速度,以最快的方式得到答案。"
|
||||||
|
},
|
||||||
|
"balanced": {
|
||||||
|
"title": "平衡",
|
||||||
|
"description": "在速度與準確度之間取得平衡"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"title": "品質(即將推出)",
|
||||||
|
"description": "取得最完整與最精確的回答"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageActions": {
|
||||||
|
"rewrite": "重寫"
|
||||||
|
},
|
||||||
|
"searchImages": {
|
||||||
|
"searchButton": "搜尋圖片"
|
||||||
|
},
|
||||||
|
"searchVideos": {
|
||||||
|
"searchButton": "搜尋影片",
|
||||||
|
"badge": "影片"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"humidity": "濕度",
|
||||||
|
"now": "現在"
|
||||||
|
},
|
||||||
|
"newsArticleWidget": {
|
||||||
|
"error": "無法載入新聞。"
|
||||||
|
},
|
||||||
|
"emptyChat": {
|
||||||
|
"title": "研究由此開始。"
|
||||||
|
},
|
||||||
|
"emptyChatMessageInput": {
|
||||||
|
"placeholder": "問我任何事..."
|
||||||
|
},
|
||||||
|
"deleteChat": {
|
||||||
|
"title": "刪除確認",
|
||||||
|
"description": "確定要刪除此聊天嗎?",
|
||||||
|
"cancel": "取消",
|
||||||
|
"delete": "刪除"
|
||||||
|
},
|
||||||
|
"themeSwitcher": {
|
||||||
|
"options": {
|
||||||
|
"light": "淺色",
|
||||||
|
"dark": "深色"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
messages/zh-TW.json
Normal file
251
messages/zh-TW.json
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "Perplexica - 與網路對話",
|
||||||
|
"description": "Perplexica 是一個能連上網路的 AI 聊天機器人。"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "Perplexica - 與網路對話",
|
||||||
|
"shortName": "Perplexica",
|
||||||
|
"description": "Perplexica 是一個能連上網路的 AI 聊天機器人。"
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "首頁",
|
||||||
|
"discover": "探索",
|
||||||
|
"library": "資料庫",
|
||||||
|
"settings": "設定"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"appName": "Perplexica",
|
||||||
|
"exportedOn": "匯出日期:",
|
||||||
|
"citations": "參考來源:",
|
||||||
|
"user": "使用者",
|
||||||
|
"assistant": "助理",
|
||||||
|
"errors": {
|
||||||
|
"noChatModelsAvailable": "沒有可用的聊天模型",
|
||||||
|
"chatProviderNotConfigured": "看起來你還沒有設定任何聊天模型供應商,請至設定頁或設定檔進行設定。",
|
||||||
|
"noEmbeddingModelsAvailable": "沒有可用的向量嵌入模型",
|
||||||
|
"cannotSendBeforeConfigReady": "在設定就緒前無法送出訊息",
|
||||||
|
"failedToDeleteChat": "刪除聊天失敗"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navbar": {
|
||||||
|
"exportAsMarkdown": "匯出為 Markdown",
|
||||||
|
"exportAsPDF": "匯出為 PDF"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"chatExportTitle": "聊天匯出:{title}"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"conditions": {
|
||||||
|
"clear": "晴朗",
|
||||||
|
"mainlyClear": "大致晴朗",
|
||||||
|
"partlyCloudy": "局部多雲",
|
||||||
|
"cloudy": "多雲",
|
||||||
|
"fog": "霧",
|
||||||
|
"lightDrizzle": "小毛毛雨",
|
||||||
|
"moderateDrizzle": "中等毛毛雨",
|
||||||
|
"denseDrizzle": "大毛毛雨",
|
||||||
|
"lightFreezingDrizzle": "輕微冰霧雨",
|
||||||
|
"denseFreezingDrizzle": "強冰霧雨",
|
||||||
|
"slightRain": "小雨",
|
||||||
|
"moderateRain": "中雨",
|
||||||
|
"heavyRain": "大雨",
|
||||||
|
"lightFreezingRain": "輕微凍雨",
|
||||||
|
"heavyFreezingRain": "強凍雨",
|
||||||
|
"slightSnowFall": "小雪",
|
||||||
|
"moderateSnowFall": "中雪",
|
||||||
|
"heavySnowFall": "大雪",
|
||||||
|
"snow": "降雪",
|
||||||
|
"slightRainShowers": "短暫小雨",
|
||||||
|
"moderateRainShowers": "短暫中雨",
|
||||||
|
"heavyRainShowers": "短暫大雨",
|
||||||
|
"slightSnowShowers": "短暫小雪",
|
||||||
|
"moderateSnowShowers": "短暫中雪",
|
||||||
|
"heavySnowShowers": "短暫大雪",
|
||||||
|
"thunderstorm": "雷雨",
|
||||||
|
"thunderstormSlightHail": "雷雨伴隨小冰雹",
|
||||||
|
"thunderstormHeavyHail": "雷雨伴隨大冰雹"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"home": {
|
||||||
|
"title": "聊天 - Perplexica",
|
||||||
|
"description": "連上網路聊聊天,和 Perplexica 對話。"
|
||||||
|
},
|
||||||
|
"discover": {
|
||||||
|
"title": "探索",
|
||||||
|
"topics": {
|
||||||
|
"tech": "科技與科學",
|
||||||
|
"finance": "財經",
|
||||||
|
"art": "藝術與文化",
|
||||||
|
"sports": "運動",
|
||||||
|
"entertainment": "娛樂"
|
||||||
|
},
|
||||||
|
"errorFetchingData": "取得資料時發生錯誤"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"title": "資料庫",
|
||||||
|
"empty": "沒有找到任何聊天紀錄。",
|
||||||
|
"ago": "{time} 前"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "設定",
|
||||||
|
"sections": {
|
||||||
|
"preferences": "偏好設定",
|
||||||
|
"automaticSearch": "自動搜尋",
|
||||||
|
"systemInstructions": "系統指示",
|
||||||
|
"modelSettings": "模型設定",
|
||||||
|
"apiKeys": "API 金鑰"
|
||||||
|
},
|
||||||
|
"preferences": {
|
||||||
|
"theme": "主題",
|
||||||
|
"measurementUnits": "度量單位",
|
||||||
|
"language": "語言",
|
||||||
|
"metric": "公制",
|
||||||
|
"imperial": "英制"
|
||||||
|
},
|
||||||
|
"automaticSearch": {
|
||||||
|
"image": {
|
||||||
|
"title": "自動圖片搜尋",
|
||||||
|
"desc": "在聊天回應中自動搜尋相關圖片"
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"title": "自動影片搜尋",
|
||||||
|
"desc": "在聊天回應中自動搜尋相關影片"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"chatProvider": "聊天模型供應商",
|
||||||
|
"chat": "聊天模型",
|
||||||
|
"noModels": "沒有可用的模型",
|
||||||
|
"invalidProvider": "供應商無效,請檢查後端日誌",
|
||||||
|
"custom": {
|
||||||
|
"modelName": "模型名稱",
|
||||||
|
"apiKey": "自訂 OpenAI API 金鑰",
|
||||||
|
"baseUrl": "自訂 OpenAI Base URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"embedding": {
|
||||||
|
"provider": "向量嵌入供應商",
|
||||||
|
"model": "向量嵌入模型"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"openaiApiKey": "OpenAI API 金鑰",
|
||||||
|
"ollamaApiUrl": "Ollama API 位址",
|
||||||
|
"groqApiKey": "GROQ API 金鑰",
|
||||||
|
"anthropicApiKey": "Anthropic API 金鑰",
|
||||||
|
"geminiApiKey": "Gemini API 金鑰",
|
||||||
|
"deepseekApiKey": "Deepseek API 金鑰",
|
||||||
|
"aimlApiKey": "AI/ML API 金鑰",
|
||||||
|
"lmStudioApiUrl": "LM Studio API 位址"
|
||||||
|
},
|
||||||
|
"systemInstructions": {
|
||||||
|
"placeholder": "任何要給 LLM 的特別指示"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"common": {
|
||||||
|
"viewMore": "查看另外 {count} 項"
|
||||||
|
},
|
||||||
|
"messageInput": {
|
||||||
|
"placeholder": "提出追問"
|
||||||
|
},
|
||||||
|
"messageBox": {
|
||||||
|
"sources": "參考來源",
|
||||||
|
"answer": "回答",
|
||||||
|
"related": "相關內容"
|
||||||
|
},
|
||||||
|
"copilot": {
|
||||||
|
"label": "Copilot"
|
||||||
|
},
|
||||||
|
"attach": {
|
||||||
|
"attachedFiles": "已附加的檔案",
|
||||||
|
"add": "新增",
|
||||||
|
"clear": "清除",
|
||||||
|
"attach": "附加",
|
||||||
|
"uploading": "上傳中...",
|
||||||
|
"files": "{count} 個檔案"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"button": "焦點",
|
||||||
|
"modes": {
|
||||||
|
"webSearch": {
|
||||||
|
"title": "全部",
|
||||||
|
"description": "在整個網路上搜尋"
|
||||||
|
},
|
||||||
|
"academicSearch": {
|
||||||
|
"title": "學術",
|
||||||
|
"description": "搜尋已發表的學術論文"
|
||||||
|
},
|
||||||
|
"writingAssistant": {
|
||||||
|
"title": "寫作",
|
||||||
|
"description": "不搜尋網路,直接聊天"
|
||||||
|
},
|
||||||
|
"wolframAlphaSearch": {
|
||||||
|
"title": "Wolfram Alpha",
|
||||||
|
"description": "計算型知識引擎"
|
||||||
|
},
|
||||||
|
"youtubeSearch": {
|
||||||
|
"title": "YouTube",
|
||||||
|
"description": "搜尋與觀看影片"
|
||||||
|
},
|
||||||
|
"redditSearch": {
|
||||||
|
"title": "Reddit",
|
||||||
|
"description": "搜尋討論與觀點"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optimization": {
|
||||||
|
"modes": {
|
||||||
|
"speed": {
|
||||||
|
"title": "速度",
|
||||||
|
"description": "優先速度,以最快的方式得到答案。"
|
||||||
|
},
|
||||||
|
"balanced": {
|
||||||
|
"title": "平衡",
|
||||||
|
"description": "在速度與準確度之間取得平衡"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"title": "品質(即將推出)",
|
||||||
|
"description": "取得最完整與最精確的回答"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageActions": {
|
||||||
|
"rewrite": "重寫"
|
||||||
|
},
|
||||||
|
"searchImages": {
|
||||||
|
"searchButton": "搜尋圖片"
|
||||||
|
},
|
||||||
|
"searchVideos": {
|
||||||
|
"searchButton": "搜尋影片",
|
||||||
|
"badge": "影片"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"humidity": "濕度",
|
||||||
|
"now": "現在"
|
||||||
|
},
|
||||||
|
"newsArticleWidget": {
|
||||||
|
"error": "無法載入新聞。"
|
||||||
|
},
|
||||||
|
"emptyChat": {
|
||||||
|
"title": "研究從這裡開始。"
|
||||||
|
},
|
||||||
|
"emptyChatMessageInput": {
|
||||||
|
"placeholder": "問我任何事..."
|
||||||
|
},
|
||||||
|
"deleteChat": {
|
||||||
|
"title": "刪除確認",
|
||||||
|
"description": "確定要刪除此聊天嗎?",
|
||||||
|
"cancel": "取消",
|
||||||
|
"delete": "刪除"
|
||||||
|
},
|
||||||
|
"themeSwitcher": {
|
||||||
|
"options": {
|
||||||
|
"light": "淺色",
|
||||||
|
"dark": "深色"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
|
@ -11,4 +13,5 @@ const nextConfig = {
|
||||||
serverExternalPackages: ['pdf-parse'],
|
serverExternalPackages: ['pdf-parse'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
export default withNextIntl(nextConfig);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"mammoth": "^1.9.1",
|
"mammoth": "^1.9.1",
|
||||||
"markdown-to-jsx": "^7.7.2",
|
"markdown-to-jsx": "^7.7.2",
|
||||||
"next": "^15.2.2",
|
"next": "^15.2.2",
|
||||||
|
"next-intl": "^4.3.4",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
|
|
||||||
36
public/fonts/LICENSE
Normal file
36
public/fonts/LICENSE
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
Noto Sans Traditional Chinese
|
||||||
|
|
||||||
|
License
|
||||||
|
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1 . This license is copied below, and is also available with a FAQ at: https://openfontlicense.org
|
||||||
|
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
|
||||||
|
|
||||||
|
Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
|
||||||
|
Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
|
||||||
|
No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
|
||||||
|
The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
|
||||||
|
The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
BIN
public/fonts/NotoSansTC-Bold.ttf
Normal file
BIN
public/fonts/NotoSansTC-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/NotoSansTC-Regular.ttf
Normal file
BIN
public/fonts/NotoSansTC-Regular.ttf
Normal file
Binary file not shown.
13
public/fonts/README.md
Normal file
13
public/fonts/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
Place font files here for client-side PDF generation.
|
||||||
|
|
||||||
|
Recommended (Noto Sans TC - SIL Open Font License 1.1):
|
||||||
|
|
||||||
|
- NotoSansTC-Regular.ttf
|
||||||
|
- NotoSansTC-Bold.ttf
|
||||||
|
|
||||||
|
You can download from:
|
||||||
|
|
||||||
|
- https://fonts.google.com/noto/specimen/Noto+Sans+TC
|
||||||
|
- https://github.com/googlefonts/noto-cjk (Sans/TTF)
|
||||||
|
|
||||||
|
These fonts will be fetched via relative URLs at runtime by jsPDF.
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { searchSearxng } from '@/lib/searxng';
|
import { searchSearxng } from '@/lib/searxng';
|
||||||
|
import { getLocale } from 'next-intl/server';
|
||||||
|
|
||||||
const websitesForTopic = {
|
const websitesForTopic = {
|
||||||
tech: {
|
tech: {
|
||||||
|
|
@ -37,6 +38,10 @@ export const GET = async (req: Request) => {
|
||||||
|
|
||||||
let data = [];
|
let data = [];
|
||||||
|
|
||||||
|
// derive base language from current locale (e.g., zh-TW -> zh)
|
||||||
|
const locale = await getLocale();
|
||||||
|
const searxLanguage = 'en';
|
||||||
|
|
||||||
if (mode === 'normal') {
|
if (mode === 'normal') {
|
||||||
const seenUrls = new Set();
|
const seenUrls = new Set();
|
||||||
|
|
||||||
|
|
@ -46,9 +51,9 @@ export const GET = async (req: Request) => {
|
||||||
selectedTopic.query.map(async (query) => {
|
selectedTopic.query.map(async (query) => {
|
||||||
return (
|
return (
|
||||||
await searchSearxng(`site:${link} ${query}`, {
|
await searchSearxng(`site:${link} ${query}`, {
|
||||||
engines: ['bing news'],
|
engines: ['google news', 'bing news'],
|
||||||
pageno: 1,
|
pageno: 1,
|
||||||
language: 'en',
|
language: searxLanguage,
|
||||||
})
|
})
|
||||||
).results;
|
).results;
|
||||||
}),
|
}),
|
||||||
|
|
@ -68,9 +73,9 @@ export const GET = async (req: Request) => {
|
||||||
await searchSearxng(
|
await searchSearxng(
|
||||||
`site:${selectedTopic.links[Math.floor(Math.random() * selectedTopic.links.length)]} ${selectedTopic.query[Math.floor(Math.random() * selectedTopic.query.length)]}`,
|
`site:${selectedTopic.links[Math.floor(Math.random() * selectedTopic.links.length)]} ${selectedTopic.query[Math.floor(Math.random() * selectedTopic.query.length)]}`,
|
||||||
{
|
{
|
||||||
engines: ['bing news'],
|
engines: ['google news', 'bing news'],
|
||||||
pageno: 1,
|
pageno: 1,
|
||||||
language: 'en',
|
language: searxLanguage,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
).results;
|
).results;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
export const POST = async (req: Request) => {
|
export const POST = async (req: Request) => {
|
||||||
try {
|
try {
|
||||||
|
const t = await getTranslations('weather.conditions');
|
||||||
const body: {
|
const body: {
|
||||||
lat: number;
|
lat: number;
|
||||||
lng: number;
|
lng: number;
|
||||||
|
|
@ -58,104 +61,104 @@ export const POST = async (req: Request) => {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 0:
|
case 0:
|
||||||
weather.icon = `clear-${dayOrNight}`;
|
weather.icon = `clear-${dayOrNight}`;
|
||||||
weather.condition = 'Clear';
|
weather.condition = t('clear');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
weather.condition = 'Mainly Clear';
|
weather.condition = t('mainlyClear');
|
||||||
case 2:
|
case 2:
|
||||||
weather.condition = 'Partly Cloudy';
|
weather.condition = t('partlyCloudy');
|
||||||
case 3:
|
case 3:
|
||||||
weather.icon = `cloudy-1-${dayOrNight}`;
|
weather.icon = `cloudy-1-${dayOrNight}`;
|
||||||
weather.condition = 'Cloudy';
|
weather.condition = t('cloudy');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 45:
|
case 45:
|
||||||
weather.condition = 'Fog';
|
weather.condition = t('fog');
|
||||||
case 48:
|
case 48:
|
||||||
weather.icon = `fog-${dayOrNight}`;
|
weather.icon = `fog-${dayOrNight}`;
|
||||||
weather.condition = 'Fog';
|
weather.condition = t('fog');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 51:
|
case 51:
|
||||||
weather.condition = 'Light Drizzle';
|
weather.condition = t('lightDrizzle');
|
||||||
case 53:
|
case 53:
|
||||||
weather.condition = 'Moderate Drizzle';
|
weather.condition = t('moderateDrizzle');
|
||||||
case 55:
|
case 55:
|
||||||
weather.icon = `rainy-1-${dayOrNight}`;
|
weather.icon = `rainy-1-${dayOrNight}`;
|
||||||
weather.condition = 'Dense Drizzle';
|
weather.condition = t('denseDrizzle');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 56:
|
case 56:
|
||||||
weather.condition = 'Light Freezing Drizzle';
|
weather.condition = t('lightFreezingDrizzle');
|
||||||
case 57:
|
case 57:
|
||||||
weather.icon = `frost-${dayOrNight}`;
|
weather.icon = `frost-${dayOrNight}`;
|
||||||
weather.condition = 'Dense Freezing Drizzle';
|
weather.condition = t('denseFreezingDrizzle');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 61:
|
case 61:
|
||||||
weather.condition = 'Slight Rain';
|
weather.condition = t('slightRain');
|
||||||
case 63:
|
case 63:
|
||||||
weather.condition = 'Moderate Rain';
|
weather.condition = t('moderateRain');
|
||||||
case 65:
|
case 65:
|
||||||
weather.condition = 'Heavy Rain';
|
weather.condition = t('heavyRain');
|
||||||
weather.icon = `rainy-2-${dayOrNight}`;
|
weather.icon = `rainy-2-${dayOrNight}`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 66:
|
case 66:
|
||||||
weather.condition = 'Light Freezing Rain';
|
weather.condition = t('lightFreezingRain');
|
||||||
case 67:
|
case 67:
|
||||||
weather.condition = 'Heavy Freezing Rain';
|
weather.condition = t('heavyFreezingRain');
|
||||||
weather.icon = 'rain-and-sleet-mix';
|
weather.icon = 'rain-and-sleet-mix';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 71:
|
case 71:
|
||||||
weather.condition = 'Slight Snow Fall';
|
weather.condition = t('slightSnowFall');
|
||||||
case 73:
|
case 73:
|
||||||
weather.condition = 'Moderate Snow Fall';
|
weather.condition = t('moderateSnowFall');
|
||||||
case 75:
|
case 75:
|
||||||
weather.condition = 'Heavy Snow Fall';
|
weather.condition = t('heavySnowFall');
|
||||||
weather.icon = `snowy-2-${dayOrNight}`;
|
weather.icon = `snowy-2-${dayOrNight}`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 77:
|
case 77:
|
||||||
weather.condition = 'Snow';
|
weather.condition = t('snow');
|
||||||
weather.icon = `snowy-1-${dayOrNight}`;
|
weather.icon = `snowy-1-${dayOrNight}`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 80:
|
case 80:
|
||||||
weather.condition = 'Slight Rain Showers';
|
weather.condition = t('slightRainShowers');
|
||||||
case 81:
|
case 81:
|
||||||
weather.condition = 'Moderate Rain Showers';
|
weather.condition = t('moderateRainShowers');
|
||||||
case 82:
|
case 82:
|
||||||
weather.condition = 'Heavy Rain Showers';
|
weather.condition = t('heavyRainShowers');
|
||||||
weather.icon = `rainy-3-${dayOrNight}`;
|
weather.icon = `rainy-3-${dayOrNight}`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 85:
|
case 85:
|
||||||
weather.condition = 'Slight Snow Showers';
|
weather.condition = t('slightSnowShowers');
|
||||||
case 86:
|
case 86:
|
||||||
weather.condition = 'Moderate Snow Showers';
|
weather.condition = t('moderateSnowShowers');
|
||||||
case 87:
|
case 87:
|
||||||
weather.condition = 'Heavy Snow Showers';
|
weather.condition = t('heavySnowShowers');
|
||||||
weather.icon = `snowy-3-${dayOrNight}`;
|
weather.icon = `snowy-3-${dayOrNight}`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 95:
|
case 95:
|
||||||
weather.condition = 'Thunderstorm';
|
weather.condition = t('thunderstorm');
|
||||||
weather.icon = `scattered-thunderstorms-${dayOrNight}`;
|
weather.icon = `scattered-thunderstorms-${dayOrNight}`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 96:
|
case 96:
|
||||||
weather.condition = 'Thunderstorm with Slight Hail';
|
weather.condition = t('thunderstormSlightHail');
|
||||||
case 99:
|
case 99:
|
||||||
weather.condition = 'Thunderstorm with Heavy Hail';
|
weather.condition = t('thunderstormHeavyHail');
|
||||||
weather.icon = 'severe-thunderstorm';
|
weather.icon = 'severe-thunderstorm';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
weather.icon = `clear-${dayOrNight}`;
|
weather.icon = `clear-${dayOrNight}`;
|
||||||
weather.condition = 'Clear';
|
weather.condition = t('clear');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
|
@ -13,35 +15,24 @@ interface Discover {
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const topics: { key: string; display: string }[] = [
|
const topics: {
|
||||||
{
|
key: 'tech' | 'finance' | 'art' | 'sports' | 'entertainment';
|
||||||
display: 'Tech & Science',
|
}[] = [
|
||||||
key: 'tech',
|
{ key: 'tech' },
|
||||||
},
|
{ key: 'finance' },
|
||||||
{
|
{ key: 'art' },
|
||||||
display: 'Finance',
|
{ key: 'sports' },
|
||||||
key: 'finance',
|
{ key: 'entertainment' },
|
||||||
},
|
|
||||||
{
|
|
||||||
display: 'Art & Culture',
|
|
||||||
key: 'art',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display: 'Sports',
|
|
||||||
key: 'sports',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display: 'Entertainment',
|
|
||||||
key: 'entertainment',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const [discover, setDiscover] = useState<Discover[] | null>(null);
|
const [discover, setDiscover] = useState<Discover[] | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [activeTopic, setActiveTopic] = useState<string>(topics[0].key);
|
const [activeTopic, setActiveTopic] = useState<string>(topics[0].key);
|
||||||
|
const t = useTranslations('pages.discover');
|
||||||
|
|
||||||
const fetchArticles = async (topic: string) => {
|
const fetchArticles = useCallback(
|
||||||
|
async (topic: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/discover?topic=${topic}`, {
|
const res = await fetch(`/api/discover?topic=${topic}`, {
|
||||||
|
|
@ -62,15 +53,17 @@ const Page = () => {
|
||||||
setDiscover(data.blogs);
|
setDiscover(data.blogs);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error fetching data:', err.message);
|
console.error('Error fetching data:', err.message);
|
||||||
toast.error('Error fetching data');
|
toast.error(t('errorFetchingData'));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchArticles(activeTopic);
|
fetchArticles(activeTopic);
|
||||||
}, [activeTopic]);
|
}, [activeTopic, fetchArticles]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -78,24 +71,24 @@ const Page = () => {
|
||||||
<div className="flex flex-col pt-4">
|
<div className="flex flex-col pt-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Search />
|
<Search />
|
||||||
<h1 className="text-3xl font-medium p-2">Discover</h1>
|
<h1 className="text-3xl font-medium p-2">{t('title')}</h1>
|
||||||
</div>
|
</div>
|
||||||
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row items-center space-x-2 overflow-x-auto">
|
<div className="flex flex-row items-center space-x-2 overflow-x-auto">
|
||||||
{topics.map((t, i) => (
|
{topics.map((topic, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-[0.1px] rounded-full text-sm px-3 py-1 text-nowrap transition duration-200 cursor-pointer',
|
'border-[0.1px] rounded-full text-sm px-3 py-1 text-nowrap transition duration-200 cursor-pointer',
|
||||||
activeTopic === t.key
|
activeTopic === topic.key
|
||||||
? 'text-cyan-300 bg-cyan-300/30 border-cyan-300/60'
|
? 'text-cyan-600 bg-cyan-100 border-cyan-300 dark:text-cyan-300 dark:bg-cyan-300/30 dark:border-cyan-300/60'
|
||||||
: 'border-white/30 text-white/70 hover:text-white hover:border-white/40 hover:bg-white/5',
|
: 'text-gray-700 border-gray-300 hover:text-gray-900 hover:bg-gray-100 hover:border-gray-400 dark:text-white/70 dark:border-white/30 dark:hover:text-white dark:hover:bg-white/5 dark:hover:border-white/40',
|
||||||
)}
|
)}
|
||||||
onClick={() => setActiveTopic(t.key)}
|
onClick={() => setActiveTopic(topic.key)}
|
||||||
>
|
>
|
||||||
<span>{t.display}</span>
|
<span>{t(`topics.${topic.key}`)}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -129,15 +122,20 @@ const Page = () => {
|
||||||
className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200"
|
className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<img
|
<div className="relative w-full aspect-video">
|
||||||
className="object-cover w-full aspect-video"
|
<Image
|
||||||
|
fill
|
||||||
|
unoptimized
|
||||||
|
className="object-cover"
|
||||||
src={
|
src={
|
||||||
new URL(item.thumbnail).origin +
|
new URL(item.thumbnail).origin +
|
||||||
new URL(item.thumbnail).pathname +
|
new URL(item.thumbnail).pathname +
|
||||||
`?id=${new URL(item.thumbnail).searchParams.get('id')}`
|
`?id=${new URL(item.thumbnail).searchParams.get('id')}`
|
||||||
}
|
}
|
||||||
alt={item.title}
|
alt={item.title}
|
||||||
|
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<div className="px-6 py-4">
|
<div className="px-6 py-4">
|
||||||
<div className="font-bold text-lg mb-2">
|
<div className="font-bold text-lg mb-2">
|
||||||
{item.title.slice(0, 100)}...
|
{item.title.slice(0, 100)}...
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ import { cn } from '@/lib/utils';
|
||||||
import Sidebar from '@/components/Sidebar';
|
import Sidebar from '@/components/Sidebar';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
import ThemeProvider from '@/components/theme/Provider';
|
import ThemeProvider from '@/components/theme/Provider';
|
||||||
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
|
import { getLocale, getTranslations } from 'next-intl/server';
|
||||||
|
import LocaleBootstrap from '@/components/LocaleBootstrap';
|
||||||
|
import { LOCALES, DEFAULT_LOCALE, type AppLocale } from '@/i18n/locales';
|
||||||
|
|
||||||
const montserrat = Montserrat({
|
const montserrat = Montserrat({
|
||||||
weight: ['300', '400', '500', '700'],
|
weight: ['300', '400', '500', '700'],
|
||||||
|
|
@ -13,20 +17,30 @@ const montserrat = Montserrat({
|
||||||
fallback: ['Arial', 'sans-serif'],
|
fallback: ['Arial', 'sans-serif'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
title: 'Perplexica - Chat with the internet',
|
const t = await getTranslations('metadata');
|
||||||
description:
|
return {
|
||||||
'Perplexica is an AI powered chatbot that is connected to the internet.',
|
title: t('title'),
|
||||||
|
description: t('description'),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const locale = await getLocale();
|
||||||
|
const appLocale: AppLocale = (LOCALES as readonly string[]).includes(
|
||||||
|
locale as string,
|
||||||
|
)
|
||||||
|
? (locale as AppLocale)
|
||||||
|
: DEFAULT_LOCALE;
|
||||||
return (
|
return (
|
||||||
<html className="h-full" lang="en" suppressHydrationWarning>
|
<html className="h-full" lang={locale} suppressHydrationWarning>
|
||||||
<body className={cn('h-full', montserrat.className)}>
|
<body className={cn('h-full', montserrat.className)}>
|
||||||
|
<LocaleBootstrap initialLocale={appLocale} />
|
||||||
|
<NextIntlClientProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Sidebar>{children}</Sidebar>
|
<Sidebar>{children}</Sidebar>
|
||||||
<Toaster
|
<Toaster
|
||||||
|
|
@ -39,6 +53,7 @@ export default function RootLayout({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
title: 'Library - Perplexica',
|
const t = await getTranslations('pages.library');
|
||||||
|
return {
|
||||||
|
title: `${t('title')} - Perplexica`,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||||
return <div>{children}</div>;
|
return <div>{children}</div>;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import DeleteChat from '@/components/DeleteChat';
|
import DeleteChat from '@/components/DeleteChat';
|
||||||
import { cn, formatTimeDifference } from '@/lib/utils';
|
import { cn, formatTimeDifference, formatRelativeTime } from '@/lib/utils';
|
||||||
import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react';
|
import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export interface Chat {
|
export interface Chat {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -16,6 +17,8 @@ export interface Chat {
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const [chats, setChats] = useState<Chat[]>([]);
|
const [chats, setChats] = useState<Chat[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const t = useTranslations('pages.library');
|
||||||
|
const locale = useLocale();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchChats = async () => {
|
const fetchChats = async () => {
|
||||||
|
|
@ -61,14 +64,14 @@ const Page = () => {
|
||||||
<div className="flex flex-col pt-4">
|
<div className="flex flex-col pt-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<BookOpenText />
|
<BookOpenText />
|
||||||
<h1 className="text-3xl font-medium p-2">Library</h1>
|
<h1 className="text-3xl font-medium p-2">{t('title')}</h1>
|
||||||
</div>
|
</div>
|
||||||
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
||||||
</div>
|
</div>
|
||||||
{chats.length === 0 && (
|
{chats.length === 0 && (
|
||||||
<div className="flex flex-row items-center justify-center min-h-screen">
|
<div className="flex flex-row items-center justify-center min-h-screen">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
No chats found.
|
{t('empty')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -94,7 +97,7 @@ const Page = () => {
|
||||||
<div className="flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70">
|
<div className="flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70">
|
||||||
<ClockIcon size={15} />
|
<ClockIcon size={15} />
|
||||||
<p className="text-xs">
|
<p className="text-xs">
|
||||||
{formatTimeDifference(new Date(), chat.createdAt)} Ago
|
{formatRelativeTime(new Date(), chat.createdAt, locale)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DeleteChat
|
<DeleteChat
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import type { MetadataRoute } from 'next';
|
import type { MetadataRoute } from 'next';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
export default function manifest(): MetadataRoute.Manifest {
|
export default async function manifest(): Promise<MetadataRoute.Manifest> {
|
||||||
|
const t = await getTranslations('manifest');
|
||||||
return {
|
return {
|
||||||
name: 'Perplexica - Chat with the internet',
|
name: t('name'),
|
||||||
short_name: 'Perplexica',
|
short_name: t('shortName'),
|
||||||
description:
|
description: t('description'),
|
||||||
'Perplexica is an AI powered chatbot that is connected to the internet.',
|
|
||||||
start_url: '/',
|
start_url: '/',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
background_color: '#0a0a0a',
|
background_color: '#0a0a0a',
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import ChatWindow from '@/components/ChatWindow';
|
import ChatWindow from '@/components/ChatWindow';
|
||||||
import { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
title: 'Chat - Perplexica',
|
const t = await getTranslations('pages.home');
|
||||||
description: 'Chat with the internet, chat with Perplexica.',
|
return {
|
||||||
|
title: t('title'),
|
||||||
|
description: t('description'),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Settings as SettingsIcon, ArrowLeft, Loader2 } from 'lucide-react';
|
import { Settings as SettingsIcon, ArrowLeft, Loader2 } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Switch } from '@headlessui/react';
|
import { Switch } from '@headlessui/react';
|
||||||
import ThemeSwitcher from '@/components/theme/Switcher';
|
import ThemeSwitcher from '@/components/theme/Switcher';
|
||||||
import { ImagesIcon, VideoIcon } from 'lucide-react';
|
import { ImagesIcon, VideoIcon } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { PROVIDER_METADATA } from '@/lib/providers';
|
import { PROVIDER_METADATA } from '@/lib/providers';
|
||||||
|
import LocaleSwitcher from '@/components/LocaleSwitcher';
|
||||||
|
import { getPromptLanguageName } from '@/i18n/locales';
|
||||||
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
|
||||||
interface SettingsType {
|
interface SettingsType {
|
||||||
chatModelProviders: {
|
chatModelProviders: {
|
||||||
|
|
@ -128,6 +131,8 @@ const SettingsSection = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
|
const t = useTranslations('pages.settings');
|
||||||
|
const locale = useLocale();
|
||||||
const [config, setConfig] = useState<SettingsType | null>(null);
|
const [config, setConfig] = useState<SettingsType | null>(null);
|
||||||
const [chatModels, setChatModels] = useState<Record<string, any>>({});
|
const [chatModels, setChatModels] = useState<Record<string, any>>({});
|
||||||
const [embeddingModels, setEmbeddingModels] = useState<Record<string, any>>(
|
const [embeddingModels, setEmbeddingModels] = useState<Record<string, any>>(
|
||||||
|
|
@ -211,7 +216,8 @@ const Page = () => {
|
||||||
localStorage.getItem('autoVideoSearch') === 'true',
|
localStorage.getItem('autoVideoSearch') === 'true',
|
||||||
);
|
);
|
||||||
|
|
||||||
setSystemInstructions(localStorage.getItem('systemInstructions')!);
|
const stored = localStorage.getItem('systemInstructions') || '';
|
||||||
|
setSystemInstructions(stripPrefixedPrompt(stored));
|
||||||
|
|
||||||
setMeasureUnit(
|
setMeasureUnit(
|
||||||
localStorage.getItem('measureUnit')! as 'Imperial' | 'Metric',
|
localStorage.getItem('measureUnit')! as 'Imperial' | 'Metric',
|
||||||
|
|
@ -223,6 +229,37 @@ const Page = () => {
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Remove prefix for UI display if it exists in stored value
|
||||||
|
const stripPrefixedPrompt = (text: string) => {
|
||||||
|
const trimmed = (text || '').trim();
|
||||||
|
const starts = 'Always respond to all non-code content and explanations in';
|
||||||
|
if (trimmed.startsWith(starts)) {
|
||||||
|
const parts = trimmed.split('\n\n');
|
||||||
|
// Drop the first block (prefix paragraph and rules)
|
||||||
|
const rest = parts.slice(1).join('\n\n');
|
||||||
|
return rest || '';
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildPrefixedPrompt = useCallback((base: string, loc: string) => {
|
||||||
|
const langName = getPromptLanguageName(loc);
|
||||||
|
const prefix = `Always respond to all non-code content and explanations in ${langName}.\nRules:\n1. All descriptions, explanations, and example clarifications must be in ${langName}.\n2. Any content inside code blocks and code comments must be entirely in English.\n3. For language-specific or technical terms, use the original term in that specific language (do not translate it).`;
|
||||||
|
const trimmed = (base || '').trim();
|
||||||
|
// If already starts with the prefix (by simple inclusion of first sentence), avoid duplicating
|
||||||
|
if (
|
||||||
|
trimmed.startsWith(
|
||||||
|
`Always respond to all non-code content and explanations in`,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// If locale changed, replace the existing first paragraph block
|
||||||
|
const parts = trimmed.split('\n\n');
|
||||||
|
const rest = parts.slice(1).join('\n\n');
|
||||||
|
return `${prefix}${rest ? '\n\n' + rest : ''}`;
|
||||||
|
}
|
||||||
|
return prefix + (trimmed ? `\n\n${trimmed}` : '');
|
||||||
|
}, []);
|
||||||
|
|
||||||
const saveConfig = async (key: string, value: any) => {
|
const saveConfig = async (key: string, value: any) => {
|
||||||
setSavingStates((prev) => ({ ...prev, [key]: true }));
|
setSavingStates((prev) => ({ ...prev, [key]: true }));
|
||||||
|
|
||||||
|
|
@ -397,7 +434,7 @@ const Page = () => {
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex flex-row space-x-0.5 items-center">
|
<div className="flex flex-row space-x-0.5 items-center">
|
||||||
<SettingsIcon size={23} />
|
<SettingsIcon size={23} />
|
||||||
<h1 className="text-3xl font-medium p-2">Settings</h1>
|
<h1 className="text-3xl font-medium p-2">{t('title')}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
|
||||||
|
|
@ -425,16 +462,16 @@ const Page = () => {
|
||||||
) : (
|
) : (
|
||||||
config && (
|
config && (
|
||||||
<div className="flex flex-col space-y-6 pb-28 lg:pb-8">
|
<div className="flex flex-col space-y-6 pb-28 lg:pb-8">
|
||||||
<SettingsSection title="Preferences">
|
<SettingsSection title={t('sections.preferences')}>
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Theme
|
{t('preferences.theme')}
|
||||||
</p>
|
</p>
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Measurement Units
|
{t('preferences.measurementUnits')}
|
||||||
</p>
|
</p>
|
||||||
<Select
|
<Select
|
||||||
value={measureUnit ?? undefined}
|
value={measureUnit ?? undefined}
|
||||||
|
|
@ -444,19 +481,34 @@ const Page = () => {
|
||||||
}}
|
}}
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
label: 'Metric',
|
label: t('preferences.metric'),
|
||||||
value: 'Metric',
|
value: 'Metric',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Imperial',
|
label: t('preferences.imperial'),
|
||||||
value: 'Imperial',
|
value: 'Imperial',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
|
{t('preferences.language')}
|
||||||
|
</p>
|
||||||
|
<LocaleSwitcher
|
||||||
|
onChange={(nextLocale) => {
|
||||||
|
// Rebuild and persist with new locale prefix; keep UI clean
|
||||||
|
const prefixed = buildPrefixedPrompt(
|
||||||
|
systemInstructions,
|
||||||
|
nextLocale,
|
||||||
|
);
|
||||||
|
saveConfig('systemInstructions', prefixed);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection title="Automatic Search">
|
<SettingsSection title={t('sections.automaticSearch')}>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors">
|
<div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
|
|
@ -468,11 +520,10 @@ const Page = () => {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-black/90 dark:text-white/90 font-medium">
|
<p className="text-sm text-black/90 dark:text-white/90 font-medium">
|
||||||
Automatic Image Search
|
{t('automaticSearch.image.title')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
|
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
|
||||||
Automatically search for relevant images in chat
|
{t('automaticSearch.image.desc')}
|
||||||
responses
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -510,11 +561,10 @@ const Page = () => {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-black/90 dark:text-white/90 font-medium">
|
<p className="text-sm text-black/90 dark:text-white/90 font-medium">
|
||||||
Automatic Video Search
|
{t('automaticSearch.video.title')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
|
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
|
||||||
Automatically search for relevant videos in chat
|
{t('automaticSearch.video.desc')}
|
||||||
responses
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -544,7 +594,7 @@ const Page = () => {
|
||||||
</div>
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection title="System Instructions">
|
<SettingsSection title={t('sections.systemInstructions')}>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<Textarea
|
<Textarea
|
||||||
value={systemInstructions ?? undefined}
|
value={systemInstructions ?? undefined}
|
||||||
|
|
@ -552,17 +602,23 @@ const Page = () => {
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSystemInstructions(e.target.value);
|
setSystemInstructions(e.target.value);
|
||||||
}}
|
}}
|
||||||
onSave={(value) => saveConfig('systemInstructions', value)}
|
onSave={(value) => {
|
||||||
|
const prefixed = buildPrefixedPrompt(value, locale);
|
||||||
|
// Keep UI as user input without prefix
|
||||||
|
setSystemInstructions(value);
|
||||||
|
saveConfig('systemInstructions', prefixed);
|
||||||
|
}}
|
||||||
|
placeholder={t('systemInstructions.placeholder')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection title="Model Settings">
|
<SettingsSection title={t('sections.modelSettings')}>
|
||||||
{config.chatModelProviders && (
|
{config.chatModelProviders && (
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Chat Model Provider
|
{t('model.chatProvider')}
|
||||||
</p>
|
</p>
|
||||||
<Select
|
<Select
|
||||||
value={selectedChatModelProvider ?? undefined}
|
value={selectedChatModelProvider ?? undefined}
|
||||||
|
|
@ -593,7 +649,7 @@ const Page = () => {
|
||||||
selectedChatModelProvider != 'custom_openai' && (
|
selectedChatModelProvider != 'custom_openai' && (
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Chat Model
|
{t('model.chat')}
|
||||||
</p>
|
</p>
|
||||||
<Select
|
<Select
|
||||||
value={selectedChatModel ?? undefined}
|
value={selectedChatModel ?? undefined}
|
||||||
|
|
@ -616,15 +672,14 @@ const Page = () => {
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
value: '',
|
value: '',
|
||||||
label: 'No models available',
|
label: t('model.noModels'),
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
value: '',
|
value: '',
|
||||||
label:
|
label: t('model.invalidProvider'),
|
||||||
'Invalid provider, please check backend logs',
|
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -640,11 +695,11 @@ const Page = () => {
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Model Name
|
{t('model.custom.modelName')}
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Model name"
|
placeholder={t('model.custom.modelName')}
|
||||||
value={config.customOpenaiModelName}
|
value={config.customOpenaiModelName}
|
||||||
isSaving={savingStates['customOpenaiModelName']}
|
isSaving={savingStates['customOpenaiModelName']}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|
@ -660,11 +715,10 @@ const Page = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Custom OpenAI API Key
|
{t('model.custom.apiKey')}
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="password"
|
||||||
placeholder="Custom OpenAI API Key"
|
|
||||||
value={config.customOpenaiApiKey}
|
value={config.customOpenaiApiKey}
|
||||||
isSaving={savingStates['customOpenaiApiKey']}
|
isSaving={savingStates['customOpenaiApiKey']}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|
@ -680,11 +734,11 @@ const Page = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Custom OpenAI Base URL
|
{t('model.custom.baseUrl')}
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Custom OpenAI Base URL"
|
placeholder={t('model.custom.baseUrl')}
|
||||||
value={config.customOpenaiApiUrl}
|
value={config.customOpenaiApiUrl}
|
||||||
isSaving={savingStates['customOpenaiApiUrl']}
|
isSaving={savingStates['customOpenaiApiUrl']}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|
@ -705,7 +759,7 @@ const Page = () => {
|
||||||
<div className="flex flex-col space-y-4 mt-4 pt-4 border-t border-light-200 dark:border-dark-200">
|
<div className="flex flex-col space-y-4 mt-4 pt-4 border-t border-light-200 dark:border-dark-200">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Embedding Model Provider
|
{t('embedding.provider')}
|
||||||
</p>
|
</p>
|
||||||
<Select
|
<Select
|
||||||
value={selectedEmbeddingModelProvider ?? undefined}
|
value={selectedEmbeddingModelProvider ?? undefined}
|
||||||
|
|
@ -735,7 +789,7 @@ const Page = () => {
|
||||||
{selectedEmbeddingModelProvider && (
|
{selectedEmbeddingModelProvider && (
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Embedding Model
|
{t('embedding.model')}
|
||||||
</p>
|
</p>
|
||||||
<Select
|
<Select
|
||||||
value={selectedEmbeddingModel ?? undefined}
|
value={selectedEmbeddingModel ?? undefined}
|
||||||
|
|
@ -758,15 +812,14 @@ const Page = () => {
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
value: '',
|
value: '',
|
||||||
label: 'No models available',
|
label: t('model.noModels'),
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
value: '',
|
value: '',
|
||||||
label:
|
label: t('model.invalidProvider'),
|
||||||
'Invalid provider, please check backend logs',
|
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -778,15 +831,14 @@ const Page = () => {
|
||||||
)}
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection title="API Keys">
|
<SettingsSection title={t('sections.apiKeys')}>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
OpenAI API Key
|
{t('api.openaiApiKey')}
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="password"
|
||||||
placeholder="OpenAI API Key"
|
|
||||||
value={config.openaiApiKey}
|
value={config.openaiApiKey}
|
||||||
isSaving={savingStates['openaiApiKey']}
|
isSaving={savingStates['openaiApiKey']}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -801,11 +853,11 @@ const Page = () => {
|
||||||
|
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Ollama API URL
|
{t('api.ollamaApiUrl')}
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Ollama API URL"
|
placeholder={t('api.ollamaApiUrl')}
|
||||||
value={config.ollamaApiUrl}
|
value={config.ollamaApiUrl}
|
||||||
isSaving={savingStates['ollamaApiUrl']}
|
isSaving={savingStates['ollamaApiUrl']}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -820,11 +872,10 @@ const Page = () => {
|
||||||
|
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
GROQ API Key
|
{t('api.groqApiKey')}
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="password"
|
||||||
placeholder="GROQ API Key"
|
|
||||||
value={config.groqApiKey}
|
value={config.groqApiKey}
|
||||||
isSaving={savingStates['groqApiKey']}
|
isSaving={savingStates['groqApiKey']}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -839,11 +890,10 @@ const Page = () => {
|
||||||
|
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Anthropic API Key
|
{t('api.anthropicApiKey')}
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="password"
|
||||||
placeholder="Anthropic API key"
|
|
||||||
value={config.anthropicApiKey}
|
value={config.anthropicApiKey}
|
||||||
isSaving={savingStates['anthropicApiKey']}
|
isSaving={savingStates['anthropicApiKey']}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -858,11 +908,10 @@ const Page = () => {
|
||||||
|
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Gemini API Key
|
{t('api.geminiApiKey')}
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="password"
|
||||||
placeholder="Gemini API key"
|
|
||||||
value={config.geminiApiKey}
|
value={config.geminiApiKey}
|
||||||
isSaving={savingStates['geminiApiKey']}
|
isSaving={savingStates['geminiApiKey']}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -877,11 +926,10 @@ const Page = () => {
|
||||||
|
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
Deepseek API Key
|
{t('api.deepseekApiKey')}
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="password"
|
||||||
placeholder="Deepseek API Key"
|
|
||||||
value={config.deepseekApiKey}
|
value={config.deepseekApiKey}
|
||||||
isSaving={savingStates['deepseekApiKey']}
|
isSaving={savingStates['deepseekApiKey']}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -896,11 +944,10 @@ const Page = () => {
|
||||||
|
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
AI/ML API Key
|
{t('api.aimlApiKey')}
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="password"
|
||||||
placeholder="AI/ML API Key"
|
|
||||||
value={config.aimlApiKey}
|
value={config.aimlApiKey}
|
||||||
isSaving={savingStates['aimlApiKey']}
|
isSaving={savingStates['aimlApiKey']}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -915,11 +962,11 @@ const Page = () => {
|
||||||
|
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-black/70 dark:text-white/70 text-sm">
|
<p className="text-black/70 dark:text-white/70 text-sm">
|
||||||
LM Studio API URL
|
{t('api.lmStudioApiUrl')}
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="LM Studio API URL"
|
placeholder={t('api.lmStudioApiUrl')}
|
||||||
value={config.lmStudioApiUrl}
|
value={config.lmStudioApiUrl}
|
||||||
isSaving={savingStates['lmStudioApiUrl']}
|
isSaving={savingStates['lmStudioApiUrl']}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import Chat from './Chat';
|
||||||
import EmptyChat from './EmptyChat';
|
import EmptyChat from './EmptyChat';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { getSuggestions } from '@/lib/actions';
|
import { getSuggestions } from '@/lib/actions';
|
||||||
import { Settings } from 'lucide-react';
|
import { Settings } from 'lucide-react';
|
||||||
|
|
@ -44,6 +45,7 @@ const checkConfig = async (
|
||||||
setEmbeddingModelProvider: (provider: EmbeddingModelProvider) => void,
|
setEmbeddingModelProvider: (provider: EmbeddingModelProvider) => void,
|
||||||
setIsConfigReady: (ready: boolean) => void,
|
setIsConfigReady: (ready: boolean) => void,
|
||||||
setHasError: (hasError: boolean) => void,
|
setHasError: (hasError: boolean) => void,
|
||||||
|
t: (key: string) => string,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
let chatModel = localStorage.getItem('chatModel');
|
let chatModel = localStorage.getItem('chatModel');
|
||||||
|
|
@ -85,7 +87,7 @@ const checkConfig = async (
|
||||||
const chatModelProvidersKeys = Object.keys(chatModelProviders);
|
const chatModelProvidersKeys = Object.keys(chatModelProviders);
|
||||||
|
|
||||||
if (!chatModelProviders || chatModelProvidersKeys.length === 0) {
|
if (!chatModelProviders || chatModelProvidersKeys.length === 0) {
|
||||||
return toast.error('No chat models available');
|
return toast.error(t('common.errors.noChatModelsAvailable'));
|
||||||
} else {
|
} else {
|
||||||
chatModelProvider =
|
chatModelProvider =
|
||||||
chatModelProvidersKeys.find(
|
chatModelProvidersKeys.find(
|
||||||
|
|
@ -98,9 +100,7 @@ const checkConfig = async (
|
||||||
chatModelProvider === 'custom_openai' &&
|
chatModelProvider === 'custom_openai' &&
|
||||||
Object.keys(chatModelProviders[chatModelProvider]).length === 0
|
Object.keys(chatModelProviders[chatModelProvider]).length === 0
|
||||||
) {
|
) {
|
||||||
toast.error(
|
toast.error(t('common.errors.chatProviderNotConfigured'));
|
||||||
"Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.",
|
|
||||||
);
|
|
||||||
return setHasError(true);
|
return setHasError(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,7 +114,7 @@ const checkConfig = async (
|
||||||
!embeddingModelProviders ||
|
!embeddingModelProviders ||
|
||||||
Object.keys(embeddingModelProviders).length === 0
|
Object.keys(embeddingModelProviders).length === 0
|
||||||
)
|
)
|
||||||
return toast.error('No embedding models available');
|
return toast.error(t('common.errors.noEmbeddingModelsAvailable'));
|
||||||
|
|
||||||
embeddingModelProvider = Object.keys(embeddingModelProviders)[0];
|
embeddingModelProvider = Object.keys(embeddingModelProviders)[0];
|
||||||
embeddingModel = Object.keys(
|
embeddingModel = Object.keys(
|
||||||
|
|
@ -152,9 +152,7 @@ const checkConfig = async (
|
||||||
chatModelProvider === 'custom_openai' &&
|
chatModelProvider === 'custom_openai' &&
|
||||||
Object.keys(chatModelProviders[chatModelProvider]).length === 0
|
Object.keys(chatModelProviders[chatModelProvider]).length === 0
|
||||||
) {
|
) {
|
||||||
toast.error(
|
toast.error(t('common.errors.chatProviderNotConfigured'));
|
||||||
"Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.",
|
|
||||||
);
|
|
||||||
return setHasError(true);
|
return setHasError(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,6 +263,7 @@ const loadMessages = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatWindow = ({ id }: { id?: string }) => {
|
const ChatWindow = ({ id }: { id?: string }) => {
|
||||||
|
const t = useTranslations();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const initialMessage = searchParams.get('q');
|
const initialMessage = searchParams.get('q');
|
||||||
|
|
||||||
|
|
@ -294,6 +293,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
||||||
setEmbeddingModelProvider,
|
setEmbeddingModelProvider,
|
||||||
setIsConfigReady,
|
setIsConfigReady,
|
||||||
setHasError,
|
setHasError,
|
||||||
|
t,
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -361,7 +361,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
||||||
) => {
|
) => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
if (!isConfigReady) {
|
if (!isConfigReady) {
|
||||||
toast.error('Cannot send message before the configuration is ready');
|
toast.error(t('common.errors.cannotSendBeforeConfigReady'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Chat } from '@/app/library/page';
|
import { Chat } from '@/app/library/page';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const DeleteChat = ({
|
const DeleteChat = ({
|
||||||
chatId,
|
chatId,
|
||||||
|
|
@ -25,6 +26,7 @@ const DeleteChat = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -37,7 +39,7 @@ const DeleteChat = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status != 200) {
|
if (res.status != 200) {
|
||||||
throw new Error('Failed to delete chat');
|
throw new Error(t('common.errors.failedToDeleteChat'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const newChats = chats.filter((chat) => chat.id !== chatId);
|
const newChats = chats.filter((chat) => chat.id !== chatId);
|
||||||
|
|
@ -48,7 +50,7 @@ const DeleteChat = ({
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.error(err.message);
|
toast.error(err.message || t('common.errors.failedToDeleteChat'));
|
||||||
} finally {
|
} finally {
|
||||||
setConfirmationDialogOpen(false);
|
setConfirmationDialogOpen(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -89,10 +91,10 @@ const DeleteChat = ({
|
||||||
>
|
>
|
||||||
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
|
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<DialogTitle className="text-lg font-medium leading-6 dark:text-white">
|
<DialogTitle className="text-lg font-medium leading-6 dark:text-white">
|
||||||
Delete Confirmation
|
{t('components.deleteChat.title')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<Description className="text-sm dark:text-white/70 text-black/70">
|
<Description className="text-sm dark:text-white/70 text-black/70">
|
||||||
Are you sure you want to delete this chat?
|
{t('components.deleteChat.description')}
|
||||||
</Description>
|
</Description>
|
||||||
<div className="flex flex-row items-end justify-end space-x-4 mt-6">
|
<div className="flex flex-row items-end justify-end space-x-4 mt-6">
|
||||||
<button
|
<button
|
||||||
|
|
@ -103,13 +105,13 @@ const DeleteChat = ({
|
||||||
}}
|
}}
|
||||||
className="text-black/50 dark:text-white/50 text-sm hover:text-black/70 hover:dark:text-white/70 transition duration-200"
|
className="text-black/50 dark:text-white/50 text-sm hover:text-black/70 hover:dark:text-white/70 transition duration-200"
|
||||||
>
|
>
|
||||||
Cancel
|
{t('components.deleteChat.cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="text-red-400 text-sm hover:text-red-500 transition duration200"
|
className="text-red-400 text-sm hover:text-red-500 transition duration200"
|
||||||
>
|
>
|
||||||
Delete
|
{t('components.deleteChat.delete')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</DialogPanel>
|
</DialogPanel>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { File } from './ChatWindow';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import WeatherWidget from './WeatherWidget';
|
import WeatherWidget from './WeatherWidget';
|
||||||
import NewsArticleWidget from './NewsArticleWidget';
|
import NewsArticleWidget from './NewsArticleWidget';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const EmptyChat = ({
|
const EmptyChat = ({
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
|
@ -26,6 +27,7 @@ const EmptyChat = ({
|
||||||
files: File[];
|
files: File[];
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('components');
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5">
|
<div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5">
|
||||||
|
|
@ -36,7 +38,7 @@ const EmptyChat = ({
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-4">
|
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-4">
|
||||||
<div className="flex flex-col items-center justify-center w-full space-y-8">
|
<div className="flex flex-col items-center justify-center w-full space-y-8">
|
||||||
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
|
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
|
||||||
Research begins here.
|
{t('emptyChat.title')}
|
||||||
</h2>
|
</h2>
|
||||||
<EmptyChatMessageInput
|
<EmptyChatMessageInput
|
||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import Focus from './MessageInputActions/Focus';
|
||||||
import Optimization from './MessageInputActions/Optimization';
|
import Optimization from './MessageInputActions/Optimization';
|
||||||
import Attach from './MessageInputActions/Attach';
|
import Attach from './MessageInputActions/Attach';
|
||||||
import { File } from './ChatWindow';
|
import { File } from './ChatWindow';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const EmptyChatMessageInput = ({
|
const EmptyChatMessageInput = ({
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
|
@ -30,6 +31,7 @@ const EmptyChatMessageInput = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
|
const t = useTranslations('components');
|
||||||
|
|
||||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
|
||||||
|
|
@ -80,7 +82,7 @@ const EmptyChatMessageInput = ({
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
minRows={2}
|
minRows={2}
|
||||||
className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
|
className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
|
||||||
placeholder="Ask anything..."
|
placeholder={t('emptyChatMessageInput.placeholder')}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-row items-center justify-between mt-4">
|
<div className="flex flex-row items-center justify-between mt-4">
|
||||||
<div className="flex flex-row items-center space-x-2 lg:space-x-4">
|
<div className="flex flex-row items-center space-x-2 lg:space-x-4">
|
||||||
|
|
|
||||||
26
src/components/LocaleBootstrap.tsx
Normal file
26
src/components/LocaleBootstrap.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { LOCALES, DEFAULT_LOCALE, type AppLocale } from '@/i18n/locales';
|
||||||
|
|
||||||
|
export default function LocaleBootstrap({
|
||||||
|
initialLocale,
|
||||||
|
}: {
|
||||||
|
initialLocale: AppLocale;
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
// 若已有 cookie,跳過
|
||||||
|
const hasCookie = /(?:^|; )locale=/.test(document.cookie);
|
||||||
|
if (hasCookie) return;
|
||||||
|
// 僅接受支援清單內的語系
|
||||||
|
const supported = new Set<string>(LOCALES as readonly string[]);
|
||||||
|
const loc = (initialLocale || DEFAULT_LOCALE) as string;
|
||||||
|
const chosen = Array.from(supported).find(
|
||||||
|
(s) => s.toLowerCase() === loc.toLowerCase(),
|
||||||
|
) as AppLocale | undefined;
|
||||||
|
const finalLocale: AppLocale = chosen || DEFAULT_LOCALE;
|
||||||
|
document.cookie = `locale=${finalLocale}; Path=/; Max-Age=31536000; SameSite=Lax`;
|
||||||
|
}, [initialLocale]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
61
src/components/LocaleSwitcher.tsx
Normal file
61
src/components/LocaleSwitcher.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useLocale } from 'next-intl';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import {
|
||||||
|
LOCALES,
|
||||||
|
DEFAULT_LOCALE,
|
||||||
|
type AppLocale,
|
||||||
|
LOCALE_LABELS,
|
||||||
|
} from '@/i18n/locales';
|
||||||
|
|
||||||
|
function setLocaleCookie(value: AppLocale) {
|
||||||
|
const oneYear = 60 * 60 * 24 * 365;
|
||||||
|
const isSecure =
|
||||||
|
typeof window !== 'undefined' && window.location.protocol === 'https:';
|
||||||
|
document.cookie = `locale=${value}; path=/; max-age=${oneYear}; samesite=lax${isSecure ? '; secure' : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LocaleSwitcher({
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
onChange?: (next: AppLocale) => void;
|
||||||
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const current = useLocale();
|
||||||
|
const currentLocale: AppLocale = useMemo(() => {
|
||||||
|
return (LOCALES as readonly string[]).includes(current)
|
||||||
|
? (current as AppLocale)
|
||||||
|
: DEFAULT_LOCALE;
|
||||||
|
}, [current]);
|
||||||
|
|
||||||
|
const [value, setValue] = useState<AppLocale>(currentLocale);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(currentLocale);
|
||||||
|
}, [currentLocale]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = e.target.value as AppLocale;
|
||||||
|
setValue(next);
|
||||||
|
setLocaleCookie(next);
|
||||||
|
onChange?.(next);
|
||||||
|
router.refresh();
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
'bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm'
|
||||||
|
}
|
||||||
|
aria-label="Language"
|
||||||
|
>
|
||||||
|
{LOCALES.map((loc) => (
|
||||||
|
<option key={loc} value={loc}>
|
||||||
|
{LOCALE_LABELS[loc]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Check, ClipboardList } from 'lucide-react';
|
import { Check, ClipboardList } from 'lucide-react';
|
||||||
import { Message } from '../ChatWindow';
|
import { Message } from '../ChatWindow';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const Copy = ({
|
const Copy = ({
|
||||||
message,
|
message,
|
||||||
|
|
@ -10,11 +11,12 @@ const Copy = ({
|
||||||
initialMessage: string;
|
initialMessage: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const contentToCopy = `${initialMessage}${message.sources && message.sources.length > 0 && `\n\nCitations:\n${message.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`;
|
const contentToCopy = `${initialMessage}${message.sources && message.sources.length > 0 && `\n\n${t('common.citations')}\n${message.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`;
|
||||||
navigator.clipboard.writeText(contentToCopy);
|
navigator.clipboard.writeText(contentToCopy);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 1000);
|
setTimeout(() => setCopied(false), 1000);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ArrowLeftRight } from 'lucide-react';
|
import { ArrowLeftRight } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const Rewrite = ({
|
const Rewrite = ({
|
||||||
rewrite,
|
rewrite,
|
||||||
|
|
@ -7,13 +8,14 @@ const Rewrite = ({
|
||||||
rewrite: (messageId: string) => void;
|
rewrite: (messageId: string) => void;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('components');
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => rewrite(messageId)}
|
onClick={() => rewrite(messageId)}
|
||||||
className="py-2 px-3 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white flex flex-row items-center space-x-1"
|
className="py-2 px-3 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white flex flex-row items-center space-x-1"
|
||||||
>
|
>
|
||||||
<ArrowLeftRight size={18} />
|
<ArrowLeftRight size={18} />
|
||||||
<p className="text-xs font-medium">Rewrite</p>
|
<p className="text-xs font-medium">{t('messageActions.rewrite')}</p>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import SearchImages from './SearchImages';
|
||||||
import SearchVideos from './SearchVideos';
|
import SearchVideos from './SearchVideos';
|
||||||
import { useSpeech } from 'react-text-to-speech';
|
import { useSpeech } from 'react-text-to-speech';
|
||||||
import ThinkBox from './ThinkBox';
|
import ThinkBox from './ThinkBox';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const ThinkTagProcessor = ({
|
const ThinkTagProcessor = ({
|
||||||
children,
|
children,
|
||||||
|
|
@ -52,6 +53,7 @@ const MessageBox = ({
|
||||||
rewrite: (messageId: string) => void;
|
rewrite: (messageId: string) => void;
|
||||||
sendMessage: (message: string) => void;
|
sendMessage: (message: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('components');
|
||||||
const [parsedMessage, setParsedMessage] = useState(message.content);
|
const [parsedMessage, setParsedMessage] = useState(message.content);
|
||||||
const [speechMessage, setSpeechMessage] = useState(message.content);
|
const [speechMessage, setSpeechMessage] = useState(message.content);
|
||||||
const [thinkingEnded, setThinkingEnded] = useState(false);
|
const [thinkingEnded, setThinkingEnded] = useState(false);
|
||||||
|
|
@ -166,7 +168,7 @@ const MessageBox = ({
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<BookCopy className="text-black dark:text-white" size={20} />
|
<BookCopy className="text-black dark:text-white" size={20} />
|
||||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||||
Sources
|
{t('messageBox.sources')}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<MessageSources sources={message.sources} />
|
<MessageSources sources={message.sources} />
|
||||||
|
|
@ -182,7 +184,7 @@ const MessageBox = ({
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||||
Answer
|
{t('messageBox.answer')}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -216,9 +218,9 @@ const MessageBox = ({
|
||||||
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
className="p-2 text-black/70 dark:text-white/70 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
|
||||||
>
|
>
|
||||||
{speechStatus === 'started' ? (
|
{speechStatus === 'started' ? (
|
||||||
<StopCircle size={18} />
|
<StopCircle size={18} aria-label="Stop TTS" />
|
||||||
) : (
|
) : (
|
||||||
<Volume2 size={18} />
|
<Volume2 size={18} aria-label="Start TTS" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -234,7 +236,9 @@ const MessageBox = ({
|
||||||
<div className="flex flex-col space-y-3 text-black dark:text-white">
|
<div className="flex flex-col space-y-3 text-black dark:text-white">
|
||||||
<div className="flex flex-row items-center space-x-2 mt-4">
|
<div className="flex flex-row items-center space-x-2 mt-4">
|
||||||
<Layers3 />
|
<Layers3 />
|
||||||
<h3 className="text-xl font-medium">Related</h3>
|
<h3 className="text-xl font-medium">
|
||||||
|
{t('messageBox.related')}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
{message.suggestions.map((suggestion, i) => (
|
{message.suggestions.map((suggestion, i) => (
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import Attach from './MessageInputActions/Attach';
|
||||||
import CopilotToggle from './MessageInputActions/Copilot';
|
import CopilotToggle from './MessageInputActions/Copilot';
|
||||||
import { File } from './ChatWindow';
|
import { File } from './ChatWindow';
|
||||||
import AttachSmall from './MessageInputActions/AttachSmall';
|
import AttachSmall from './MessageInputActions/AttachSmall';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const MessageInput = ({
|
const MessageInput = ({
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
|
@ -22,6 +23,7 @@ const MessageInput = ({
|
||||||
files: File[];
|
files: File[];
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('components.messageInput');
|
||||||
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
const [copilotEnabled, setCopilotEnabled] = useState(false);
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [textareaRows, setTextareaRows] = useState(1);
|
const [textareaRows, setTextareaRows] = useState(1);
|
||||||
|
|
@ -95,7 +97,7 @@ const MessageInput = ({
|
||||||
setTextareaRows(Math.ceil(height / props.rowHeight));
|
setTextareaRows(Math.ceil(height / props.rowHeight));
|
||||||
}}
|
}}
|
||||||
className="transition bg-transparent dark:placeholder:text-white/50 placeholder:text-sm text-sm dark:text-white resize-none focus:outline-none w-full px-2 max-h-24 lg:max-h-36 xl:max-h-48 flex-grow flex-shrink"
|
className="transition bg-transparent dark:placeholder:text-white/50 placeholder:text-sm text-sm dark:text-white resize-none focus:outline-none w-full px-2 max-h-24 lg:max-h-36 xl:max-h-48 flex-grow flex-shrink"
|
||||||
placeholder="Ask a follow-up"
|
placeholder={t('placeholder')}
|
||||||
/>
|
/>
|
||||||
{mode === 'single' && (
|
{mode === 'single' && (
|
||||||
<div className="flex flex-row items-center space-x-4">
|
<div className="flex flex-row items-center space-x-4">
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
|
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
|
||||||
import { Fragment, useRef, useState } from 'react';
|
import { Fragment, useRef, useState } from 'react';
|
||||||
import { File as FileType } from '../ChatWindow';
|
import { File as FileType } from '../ChatWindow';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const Attach = ({
|
const Attach = ({
|
||||||
fileIds,
|
fileIds,
|
||||||
|
|
@ -22,6 +23,7 @@ const Attach = ({
|
||||||
files: FileType[];
|
files: FileType[];
|
||||||
setFiles: (files: FileType[]) => void;
|
setFiles: (files: FileType[]) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('components.attach');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const fileInputRef = useRef<any>();
|
const fileInputRef = useRef<any>();
|
||||||
|
|
||||||
|
|
@ -57,7 +59,7 @@ const Attach = ({
|
||||||
<div className="flex flex-row items-center justify-between space-x-1">
|
<div className="flex flex-row items-center justify-between space-x-1">
|
||||||
<LoaderCircle size={18} className="text-sky-400 animate-spin" />
|
<LoaderCircle size={18} className="text-sky-400 animate-spin" />
|
||||||
<p className="text-sky-400 inline whitespace-nowrap text-xs font-medium">
|
<p className="text-sky-400 inline whitespace-nowrap text-xs font-medium">
|
||||||
Uploading..
|
{t('uploading')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : files.length > 0 ? (
|
) : files.length > 0 ? (
|
||||||
|
|
@ -104,7 +106,7 @@ const Attach = ({
|
||||||
<div className="bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col">
|
<div className="bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col">
|
||||||
<div className="flex flex-row items-center justify-between px-3 py-2">
|
<div className="flex flex-row items-center justify-between px-3 py-2">
|
||||||
<h4 className="text-black dark:text-white font-medium text-sm">
|
<h4 className="text-black dark:text-white font-medium text-sm">
|
||||||
Attached files
|
{t('attachedFiles')}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="flex flex-row items-center space-x-4">
|
<div className="flex flex-row items-center space-x-4">
|
||||||
<button
|
<button
|
||||||
|
|
@ -121,7 +123,7 @@ const Attach = ({
|
||||||
hidden
|
hidden
|
||||||
/>
|
/>
|
||||||
<Plus size={18} />
|
<Plus size={18} />
|
||||||
<p className="text-xs">Add</p>
|
<p className="text-xs">{t('add')}</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -131,7 +133,7 @@ const Attach = ({
|
||||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
||||||
>
|
>
|
||||||
<Trash size={14} />
|
<Trash size={14} />
|
||||||
<p className="text-xs">Clear</p>
|
<p className="text-xs">{t('clear')}</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -177,7 +179,9 @@ const Attach = ({
|
||||||
hidden
|
hidden
|
||||||
/>
|
/>
|
||||||
<CopyPlus size={showText ? 18 : undefined} />
|
<CopyPlus size={showText ? 18 : undefined} />
|
||||||
{showText && <p className="text-xs font-medium pl-[1px]">Attach</p>}
|
{showText && (
|
||||||
|
<p className="text-xs font-medium pl-[1px] w-max">{t('attach')}</p>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
|
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
|
||||||
import { Fragment, useRef, useState } from 'react';
|
import { Fragment, useRef, useState } from 'react';
|
||||||
import { File as FileType } from '../ChatWindow';
|
import { File as FileType } from '../ChatWindow';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const AttachSmall = ({
|
const AttachSmall = ({
|
||||||
fileIds,
|
fileIds,
|
||||||
|
|
@ -20,6 +21,7 @@ const AttachSmall = ({
|
||||||
files: FileType[];
|
files: FileType[];
|
||||||
setFiles: (files: FileType[]) => void;
|
setFiles: (files: FileType[]) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('components.attach');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const fileInputRef = useRef<any>();
|
const fileInputRef = useRef<any>();
|
||||||
|
|
||||||
|
|
@ -76,7 +78,7 @@ const AttachSmall = ({
|
||||||
<div className="bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col">
|
<div className="bg-light-primary dark:bg-dark-primary border rounded-md border-light-200 dark:border-dark-200 w-full max-h-[200px] md:max-h-none overflow-y-auto flex flex-col">
|
||||||
<div className="flex flex-row items-center justify-between px-3 py-2">
|
<div className="flex flex-row items-center justify-between px-3 py-2">
|
||||||
<h4 className="text-black dark:text-white font-medium text-sm">
|
<h4 className="text-black dark:text-white font-medium text-sm">
|
||||||
Attached files
|
{t('attachedFiles')}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="flex flex-row items-center space-x-4">
|
<div className="flex flex-row items-center space-x-4">
|
||||||
<button
|
<button
|
||||||
|
|
@ -93,7 +95,7 @@ const AttachSmall = ({
|
||||||
hidden
|
hidden
|
||||||
/>
|
/>
|
||||||
<Plus size={18} />
|
<Plus size={18} />
|
||||||
<p className="text-xs">Add</p>
|
<p className="text-xs">{t('add')}</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -103,7 +105,7 @@ const AttachSmall = ({
|
||||||
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
className="flex flex-row items-center space-x-1 text-black/70 dark:text-white/70 hover:text-black hover:dark:text-white transition duration-200"
|
||||||
>
|
>
|
||||||
<Trash size={14} />
|
<Trash size={14} />
|
||||||
<p className="text-xs">Clear</p>
|
<p className="text-xs">{t('clear')}</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Switch } from '@headlessui/react';
|
import { Switch } from '@headlessui/react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const CopilotToggle = ({
|
const CopilotToggle = ({
|
||||||
copilotEnabled,
|
copilotEnabled,
|
||||||
|
|
@ -8,6 +9,7 @@ const CopilotToggle = ({
|
||||||
copilotEnabled: boolean;
|
copilotEnabled: boolean;
|
||||||
setCopilotEnabled: (enabled: boolean) => void;
|
setCopilotEnabled: (enabled: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('components.copilot');
|
||||||
return (
|
return (
|
||||||
<div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer">
|
<div className="group flex flex-row items-center space-x-1 active:scale-95 duration-200 transition cursor-pointer">
|
||||||
<Switch
|
<Switch
|
||||||
|
|
@ -15,7 +17,7 @@ const CopilotToggle = ({
|
||||||
onChange={setCopilotEnabled}
|
onChange={setCopilotEnabled}
|
||||||
className="bg-light-secondary dark:bg-dark-secondary border border-light-200/70 dark:border-dark-200 relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
|
className="bg-light-secondary dark:bg-dark-secondary border border-light-200/70 dark:border-dark-200 relative inline-flex h-5 w-10 sm:h-6 sm:w-11 items-center rounded-full"
|
||||||
>
|
>
|
||||||
<span className="sr-only">Copilot</span>
|
<span className="sr-only">{t('label')}</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
copilotEnabled
|
copilotEnabled
|
||||||
|
|
@ -34,7 +36,7 @@ const CopilotToggle = ({
|
||||||
: 'text-black/50 dark:text-white/50 group-hover:text-black dark:group-hover:text-white',
|
: 'text-black/50 dark:text-white/50 group-hover:text-black dark:group-hover:text-white',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Copilot
|
{t('label')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,45 +15,16 @@ import {
|
||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
|
import { SiReddit, SiYoutube } from '@icons-pack/react-simple-icons';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const focusModes = [
|
const focusModeIcons: Record<string, JSX.Element> = {
|
||||||
{
|
webSearch: <Globe size={20} />,
|
||||||
key: 'webSearch',
|
academicSearch: <SwatchBook size={20} />,
|
||||||
title: 'All',
|
writingAssistant: <Pencil size={16} />,
|
||||||
description: 'Searches across all of the internet',
|
wolframAlphaSearch: <BadgePercent size={20} />,
|
||||||
icon: <Globe size={20} />,
|
youtubeSearch: <SiYoutube className="h-5 w-auto mr-0.5" />,
|
||||||
},
|
redditSearch: <SiReddit className="h-5 w-auto mr-0.5" />,
|
||||||
{
|
};
|
||||||
key: 'academicSearch',
|
|
||||||
title: 'Academic',
|
|
||||||
description: 'Search in published academic papers',
|
|
||||||
icon: <SwatchBook size={20} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'writingAssistant',
|
|
||||||
title: 'Writing',
|
|
||||||
description: 'Chat without searching the web',
|
|
||||||
icon: <Pencil size={16} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'wolframAlphaSearch',
|
|
||||||
title: 'Wolfram Alpha',
|
|
||||||
description: 'Computational knowledge engine',
|
|
||||||
icon: <BadgePercent size={20} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'youtubeSearch',
|
|
||||||
title: 'Youtube',
|
|
||||||
description: 'Search and watch videos',
|
|
||||||
icon: <SiYoutube className="h-5 w-auto mr-0.5" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'redditSearch',
|
|
||||||
title: 'Reddit',
|
|
||||||
description: 'Search for discussions and opinions',
|
|
||||||
icon: <SiReddit className="h-5 w-auto mr-0.5" />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const Focus = ({
|
const Focus = ({
|
||||||
focusMode,
|
focusMode,
|
||||||
|
|
@ -62,6 +33,15 @@ const Focus = ({
|
||||||
focusMode: string;
|
focusMode: string;
|
||||||
setFocusMode: (mode: string) => void;
|
setFocusMode: (mode: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('components.focus');
|
||||||
|
const modes = [
|
||||||
|
'webSearch',
|
||||||
|
'academicSearch',
|
||||||
|
'writingAssistant',
|
||||||
|
'wolframAlphaSearch',
|
||||||
|
'youtubeSearch',
|
||||||
|
'redditSearch',
|
||||||
|
];
|
||||||
return (
|
return (
|
||||||
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg mt-[6.5px]">
|
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg mt-[6.5px]">
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
|
|
@ -70,16 +50,16 @@ const Focus = ({
|
||||||
>
|
>
|
||||||
{focusMode !== 'webSearch' ? (
|
{focusMode !== 'webSearch' ? (
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center space-x-1">
|
||||||
{focusModes.find((mode) => mode.key === focusMode)?.icon}
|
{focusModeIcons[focusMode]}
|
||||||
<p className="text-xs font-medium hidden lg:block">
|
<p className="text-xs font-medium hidden lg:block">
|
||||||
{focusModes.find((mode) => mode.key === focusMode)?.title}
|
{t(`modes.${focusMode}.title`)}
|
||||||
</p>
|
</p>
|
||||||
<ChevronDown size={20} className="-translate-x-1" />
|
<ChevronDown size={20} className="-translate-x-1" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-row items-center space-x-1">
|
<div className="flex flex-row items-center space-x-1">
|
||||||
<ScanEye size={20} />
|
<ScanEye size={20} />
|
||||||
<p className="text-xs font-medium hidden lg:block">Focus</p>
|
<p className="text-xs font-medium hidden lg:block">{t('button')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
|
|
@ -94,13 +74,13 @@ const Focus = ({
|
||||||
>
|
>
|
||||||
<PopoverPanel className="absolute z-10 w-64 md:w-[500px] left-0">
|
<PopoverPanel className="absolute z-10 w-64 md:w-[500px] left-0">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto">
|
||||||
{focusModes.map((mode, i) => (
|
{modes.map((key, i) => (
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
onClick={() => setFocusMode(mode.key)}
|
onClick={() => setFocusMode(key)}
|
||||||
key={i}
|
key={i}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition',
|
'p-2 rounded-lg flex flex-col items-start justify-start text-start space-y-2 duration-200 cursor-pointer transition',
|
||||||
focusMode === mode.key
|
focusMode === key
|
||||||
? 'bg-light-secondary dark:bg-dark-secondary'
|
? 'bg-light-secondary dark:bg-dark-secondary'
|
||||||
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
|
: 'hover:bg-light-secondary dark:hover:bg-dark-secondary',
|
||||||
)}
|
)}
|
||||||
|
|
@ -108,16 +88,18 @@ const Focus = ({
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-row items-center space-x-1',
|
'flex flex-row items-center space-x-1',
|
||||||
focusMode === mode.key
|
focusMode === key
|
||||||
? 'text-[#24A0ED]'
|
? 'text-[#24A0ED]'
|
||||||
: 'text-black dark:text-white',
|
: 'text-black dark:text-white',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{mode.icon}
|
{focusModeIcons[key]}
|
||||||
<p className="text-sm font-medium">{mode.title}</p>
|
<p className="text-sm font-medium">
|
||||||
|
{t(`modes.${key}.title`)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-black/70 dark:text-white/70 text-xs">
|
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||||
{mode.description}
|
{t(`modes.${key}.description`)}
|
||||||
</p>
|
</p>
|
||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -7,24 +7,19 @@ import {
|
||||||
Transition,
|
Transition,
|
||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const OptimizationModes = [
|
const OptimizationModes = [
|
||||||
{
|
{
|
||||||
key: 'speed',
|
key: 'speed',
|
||||||
title: 'Speed',
|
|
||||||
description: 'Prioritize speed and get the quickest possible answer.',
|
|
||||||
icon: <Zap size={20} className="text-[#FF9800]" />,
|
icon: <Zap size={20} className="text-[#FF9800]" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'balanced',
|
key: 'balanced',
|
||||||
title: 'Balanced',
|
|
||||||
description: 'Find the right balance between speed and accuracy',
|
|
||||||
icon: <Sliders size={20} className="text-[#4CAF50]" />,
|
icon: <Sliders size={20} className="text-[#4CAF50]" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'quality',
|
key: 'quality',
|
||||||
title: 'Quality (Soon)',
|
|
||||||
description: 'Get the most thorough and accurate answer',
|
|
||||||
icon: (
|
icon: (
|
||||||
<Star
|
<Star
|
||||||
size={16}
|
size={16}
|
||||||
|
|
@ -41,6 +36,7 @@ const Optimization = ({
|
||||||
optimizationMode: string;
|
optimizationMode: string;
|
||||||
setOptimizationMode: (mode: string) => void;
|
setOptimizationMode: (mode: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('components.optimization');
|
||||||
return (
|
return (
|
||||||
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
|
|
@ -53,10 +49,7 @@ const Optimization = ({
|
||||||
?.icon
|
?.icon
|
||||||
}
|
}
|
||||||
<p className="text-xs font-medium">
|
<p className="text-xs font-medium">
|
||||||
{
|
{t(`modes.${optimizationMode}.title`)}
|
||||||
OptimizationModes.find((mode) => mode.key === optimizationMode)
|
|
||||||
?.title
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
<ChevronDown size={20} />
|
<ChevronDown size={20} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -87,10 +80,12 @@ const Optimization = ({
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center space-x-1 text-black dark:text-white">
|
<div className="flex flex-row items-center space-x-1 text-black dark:text-white">
|
||||||
{mode.icon}
|
{mode.icon}
|
||||||
<p className="text-sm font-medium">{mode.title}</p>
|
<p className="text-sm font-medium">
|
||||||
|
{t(`modes.${mode.key}.title`)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-black/70 dark:text-white/70 text-xs">
|
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||||
{mode.description}
|
{t(`modes.${mode.key}.description`)}
|
||||||
</p>
|
</p>
|
||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ import {
|
||||||
import { Document } from '@langchain/core/documents';
|
import { Document } from '@langchain/core/documents';
|
||||||
import { File } from 'lucide-react';
|
import { File } from 'lucide-react';
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const MessageSources = ({ sources }: { sources: Document[] }) => {
|
const MessageSources = ({ sources }: { sources: Document[] }) => {
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const t = useTranslations('components');
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
|
|
@ -88,7 +90,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-black/50 dark:text-white/50">
|
<p className="text-xs text-black/50 dark:text-white/50">
|
||||||
View {sources.length - 3} more
|
{t('common.viewMore', { count: sources.length - 3 })}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -107,7 +109,7 @@ const MessageSources = ({ sources }: { sources: Document[] }) => {
|
||||||
>
|
>
|
||||||
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
|
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<DialogTitle className="text-lg font-medium leading-6 dark:text-white">
|
<DialogTitle className="text-lg font-medium leading-6 dark:text-white">
|
||||||
Sources
|
{t('messageBox.sources')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<div className="grid grid-cols-2 gap-2 overflow-auto max-h-[300px] mt-2 pr-2">
|
<div className="grid grid-cols-2 gap-2 overflow-auto max-h-[300px] mt-2 pr-2">
|
||||||
{sources.map((source, i) => (
|
{sources.map((source, i) => (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import { Clock, Edit, Share, Trash, FileText, FileDown } from 'lucide-react';
|
import { Clock, Edit, Share, Trash, FileText, FileDown } from 'lucide-react';
|
||||||
import { Message } from './ChatWindow';
|
import { Message } from './ChatWindow';
|
||||||
import { useEffect, useState, Fragment } from 'react';
|
import { useEffect, useState, Fragment } from 'react';
|
||||||
import { formatTimeDifference } from '@/lib/utils';
|
import {
|
||||||
|
formatTimeDifference,
|
||||||
|
formatRelativeTime,
|
||||||
|
formatDate,
|
||||||
|
} from '@/lib/utils';
|
||||||
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
import DeleteChat from './DeleteChat';
|
import DeleteChat from './DeleteChat';
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
|
|
@ -10,6 +15,15 @@ import {
|
||||||
Transition,
|
Transition,
|
||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
|
import { ensureNotoSansTC } from '@/lib/pdfFont';
|
||||||
|
|
||||||
|
type ExportLabels = {
|
||||||
|
chatExportTitle: (p: { title: string }) => string;
|
||||||
|
exportedOn: string;
|
||||||
|
user: string;
|
||||||
|
assistant: string;
|
||||||
|
citations: string;
|
||||||
|
};
|
||||||
|
|
||||||
const downloadFile = (filename: string, content: string, type: string) => {
|
const downloadFile = (filename: string, content: string, type: string) => {
|
||||||
const blob = new Blob([content], { type });
|
const blob = new Blob([content], { type });
|
||||||
|
|
@ -25,18 +39,22 @@ const downloadFile = (filename: string, content: string, type: string) => {
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportAsMarkdown = (messages: Message[], title: string) => {
|
const exportAsMarkdown = (
|
||||||
const date = new Date(messages[0]?.createdAt || Date.now()).toLocaleString();
|
messages: Message[],
|
||||||
let md = `# 💬 Chat Export: ${title}\n\n`;
|
title: string,
|
||||||
md += `*Exported on: ${date}*\n\n---\n`;
|
labels: ExportLabels,
|
||||||
messages.forEach((msg, idx) => {
|
locale: string,
|
||||||
|
) => {
|
||||||
|
const date = formatDate(messages[0]?.createdAt || Date.now(), locale);
|
||||||
|
let md = `# 💬 ${labels.chatExportTitle({ title })}\n\n`;
|
||||||
|
md += `*${labels.exportedOn} ${date}*\n\n---\n`;
|
||||||
|
messages.forEach((msg) => {
|
||||||
md += `\n---\n`;
|
md += `\n---\n`;
|
||||||
md += `**${msg.role === 'user' ? '🧑 User' : '🤖 Assistant'}**
|
md += `**${msg.role === 'user' ? `🧑 ${labels.user}` : `🤖 ${labels.assistant}`}** \n`;
|
||||||
`;
|
md += `*${formatDate(msg.createdAt, locale)}*\n\n`;
|
||||||
md += `*${new Date(msg.createdAt).toLocaleString()}*\n\n`;
|
|
||||||
md += `> ${msg.content.replace(/\n/g, '\n> ')}\n`;
|
md += `> ${msg.content.replace(/\n/g, '\n> ')}\n`;
|
||||||
if (msg.sources && msg.sources.length > 0) {
|
if (msg.sources && msg.sources.length > 0) {
|
||||||
md += `\n**Citations:**\n`;
|
md += `\n**${labels.citations}**\n`;
|
||||||
msg.sources.forEach((src: any, i: number) => {
|
msg.sources.forEach((src: any, i: number) => {
|
||||||
const url = src.metadata?.url || '';
|
const url = src.metadata?.url || '';
|
||||||
md += `- [${i + 1}] [${url}](${url})\n`;
|
md += `- [${i + 1}] [${url}](${url})\n`;
|
||||||
|
|
@ -47,33 +65,45 @@ const exportAsMarkdown = (messages: Message[], title: string) => {
|
||||||
downloadFile(`${title || 'chat'}.md`, md, 'text/markdown');
|
downloadFile(`${title || 'chat'}.md`, md, 'text/markdown');
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportAsPDF = (messages: Message[], title: string) => {
|
const exportAsPDF = async (
|
||||||
|
messages: Message[],
|
||||||
|
title: string,
|
||||||
|
labels: ExportLabels,
|
||||||
|
locale: string,
|
||||||
|
) => {
|
||||||
const doc = new jsPDF();
|
const doc = new jsPDF();
|
||||||
const date = new Date(messages[0]?.createdAt || Date.now()).toLocaleString();
|
// Ensure CJK-capable font is available, then set fonts
|
||||||
|
try {
|
||||||
|
await ensureNotoSansTC(doc);
|
||||||
|
doc.setFont('NotoSansTC', 'normal');
|
||||||
|
} catch (e) {
|
||||||
|
// If network fails, fallback to default font (may garble CJK)
|
||||||
|
}
|
||||||
|
const date = formatDate(messages[0]?.createdAt || Date.now(), locale);
|
||||||
let y = 15;
|
let y = 15;
|
||||||
const pageHeight = doc.internal.pageSize.height;
|
const pageHeight = doc.internal.pageSize.height;
|
||||||
doc.setFontSize(18);
|
doc.setFontSize(18);
|
||||||
doc.text(`Chat Export: ${title}`, 10, y);
|
doc.text(labels.chatExportTitle({ title }), 10, y);
|
||||||
y += 8;
|
y += 8;
|
||||||
doc.setFontSize(11);
|
doc.setFontSize(11);
|
||||||
doc.setTextColor(100);
|
doc.setTextColor(100);
|
||||||
doc.text(`Exported on: ${date}`, 10, y);
|
doc.text(`${labels.exportedOn} ${date}`, 10, y);
|
||||||
y += 8;
|
y += 8;
|
||||||
doc.setDrawColor(200);
|
doc.setDrawColor(200);
|
||||||
doc.line(10, y, 200, y);
|
doc.line(10, y, 200, y);
|
||||||
y += 6;
|
y += 6;
|
||||||
doc.setTextColor(30);
|
doc.setTextColor(30);
|
||||||
messages.forEach((msg, idx) => {
|
messages.forEach((msg) => {
|
||||||
if (y > pageHeight - 30) {
|
if (y > pageHeight - 30) {
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
y = 15;
|
y = 15;
|
||||||
}
|
}
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('NotoSansTC', 'bold');
|
||||||
doc.text(`${msg.role === 'user' ? 'User' : 'Assistant'}`, 10, y);
|
doc.text(`${msg.role === 'user' ? labels.user : labels.assistant}`, 10, y);
|
||||||
doc.setFont('helvetica', 'normal');
|
doc.setFont('NotoSansTC', 'normal');
|
||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
doc.setTextColor(120);
|
doc.setTextColor(120);
|
||||||
doc.text(`${new Date(msg.createdAt).toLocaleString()}`, 40, y);
|
doc.text(`${formatDate(msg.createdAt, locale)}`, 40, y);
|
||||||
y += 6;
|
y += 6;
|
||||||
doc.setTextColor(30);
|
doc.setTextColor(30);
|
||||||
doc.setFontSize(12);
|
doc.setFontSize(12);
|
||||||
|
|
@ -93,7 +123,7 @@ const exportAsPDF = (messages: Message[], title: string) => {
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
y = 15;
|
y = 15;
|
||||||
}
|
}
|
||||||
doc.text('Citations:', 12, y);
|
doc.text(labels.citations, 12, y);
|
||||||
y += 5;
|
y += 5;
|
||||||
msg.sources.forEach((src: any, i: number) => {
|
msg.sources.forEach((src: any, i: number) => {
|
||||||
const url = src.metadata?.url || '';
|
const url = src.metadata?.url || '';
|
||||||
|
|
@ -126,7 +156,10 @@ const Navbar = ({
|
||||||
chatId: string;
|
chatId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [title, setTitle] = useState<string>('');
|
const [title, setTitle] = useState<string>('');
|
||||||
const [timeAgo, setTimeAgo] = useState<string>('');
|
const tCommon = useTranslations('common');
|
||||||
|
const tNavbar = useTranslations('navbar');
|
||||||
|
const tExport = useTranslations('export');
|
||||||
|
const locale = useLocale();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
|
|
@ -135,28 +168,11 @@ const Navbar = ({
|
||||||
? `${messages[0].content.substring(0, 20).trim()}...`
|
? `${messages[0].content.substring(0, 20).trim()}...`
|
||||||
: messages[0].content;
|
: messages[0].content;
|
||||||
setTitle(newTitle);
|
setTitle(newTitle);
|
||||||
const newTimeAgo = formatTimeDifference(
|
// title already set above
|
||||||
new Date(),
|
|
||||||
messages[0].createdAt,
|
|
||||||
);
|
|
||||||
setTimeAgo(newTimeAgo);
|
|
||||||
}
|
}
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
useEffect(() => {
|
// Removed per-locale relative time uses render-time computation
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
if (messages.length > 0) {
|
|
||||||
const newTimeAgo = formatTimeDifference(
|
|
||||||
new Date(),
|
|
||||||
messages[0].createdAt,
|
|
||||||
);
|
|
||||||
setTimeAgo(newTimeAgo);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed z-40 top-0 left-0 right-0 px-4 lg:pl-[104px] lg:pr-6 lg:px-8 flex flex-row items-center justify-between w-full py-4 text-sm text-black dark:text-white/70 border-b bg-light-primary dark:bg-dark-primary border-light-100 dark:border-dark-200">
|
<div className="fixed z-40 top-0 left-0 right-0 px-4 lg:pl-[104px] lg:pr-6 lg:px-8 flex flex-row items-center justify-between w-full py-4 text-sm text-black dark:text-white/70 border-b bg-light-primary dark:bg-dark-primary border-light-100 dark:border-dark-200">
|
||||||
|
|
@ -168,7 +184,13 @@ const Navbar = ({
|
||||||
</a>
|
</a>
|
||||||
<div className="hidden lg:flex flex-row items-center justify-center space-x-2">
|
<div className="hidden lg:flex flex-row items-center justify-center space-x-2">
|
||||||
<Clock size={17} />
|
<Clock size={17} />
|
||||||
<p className="text-xs">{timeAgo} ago</p>
|
<p className="text-xs">
|
||||||
|
{formatRelativeTime(
|
||||||
|
new Date(),
|
||||||
|
messages[0]?.createdAt || new Date(),
|
||||||
|
locale,
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="hidden lg:flex">{title}</p>
|
<p className="hidden lg:flex">{title}</p>
|
||||||
|
|
||||||
|
|
@ -190,17 +212,43 @@ const Navbar = ({
|
||||||
<div className="flex flex-col py-3 px-3 gap-2">
|
<div className="flex flex-col py-3 px-3 gap-2">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium"
|
className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium"
|
||||||
onClick={() => exportAsMarkdown(messages, title || '')}
|
onClick={async () => {
|
||||||
|
exportAsMarkdown(
|
||||||
|
messages,
|
||||||
|
title || '',
|
||||||
|
{
|
||||||
|
chatExportTitle: (p) => tExport('chatExportTitle', p),
|
||||||
|
exportedOn: tCommon('exportedOn'),
|
||||||
|
user: tCommon('user'),
|
||||||
|
assistant: tCommon('assistant'),
|
||||||
|
citations: tCommon('citations'),
|
||||||
|
},
|
||||||
|
locale,
|
||||||
|
);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<FileText size={17} className="text-[#24A0ED]" />
|
<FileText size={17} className="text-[#24A0ED]" />
|
||||||
Export as Markdown
|
{tNavbar('exportAsMarkdown')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium"
|
className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium"
|
||||||
onClick={() => exportAsPDF(messages, title || '')}
|
onClick={async () => {
|
||||||
|
await exportAsPDF(
|
||||||
|
messages,
|
||||||
|
title || '',
|
||||||
|
{
|
||||||
|
chatExportTitle: (p) => tExport('chatExportTitle', p),
|
||||||
|
exportedOn: tCommon('exportedOn'),
|
||||||
|
user: tCommon('user'),
|
||||||
|
assistant: tCommon('assistant'),
|
||||||
|
citations: tCommon('citations'),
|
||||||
|
},
|
||||||
|
locale,
|
||||||
|
);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<FileDown size={17} className="text-[#24A0ED]" />
|
<FileDown size={17} className="text-[#24A0ED]" />
|
||||||
Export as PDF
|
{tNavbar('exportAsPDF')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
interface Article {
|
interface Article {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -8,6 +10,7 @@ interface Article {
|
||||||
}
|
}
|
||||||
|
|
||||||
const NewsArticleWidget = () => {
|
const NewsArticleWidget = () => {
|
||||||
|
const t = useTranslations('components');
|
||||||
const [article, setArticle] = useState<Article | null>(null);
|
const [article, setArticle] = useState<Article | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
|
@ -39,13 +42,15 @@ const NewsArticleWidget = () => {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="w-full text-xs text-red-400">Could not load news.</div>
|
<div className="w-full text-xs text-red-400">
|
||||||
|
{t('newsArticleWidget.error')}
|
||||||
|
</div>
|
||||||
) : article ? (
|
) : article ? (
|
||||||
<a
|
<a
|
||||||
href={`/?q=Summary: ${article.url}`}
|
href={`/?q=Summary: ${article.url}`}
|
||||||
className="flex flex-row items-center w-full h-full group"
|
className="flex flex-row items-center w-full h-full group"
|
||||||
>
|
>
|
||||||
<img
|
<Image
|
||||||
className="object-cover rounded-lg w-16 min-w-16 max-w-16 h-16 min-h-16 max-h-16 border border-light-200 dark:border-dark-200 bg-light-200 dark:bg-dark-200 group-hover:opacity-90 transition"
|
className="object-cover rounded-lg w-16 min-w-16 max-w-16 h-16 min-h-16 max-h-16 border border-light-200 dark:border-dark-200 bg-light-200 dark:bg-dark-200 group-hover:opacity-90 transition"
|
||||||
src={
|
src={
|
||||||
new URL(article.thumbnail).origin +
|
new URL(article.thumbnail).origin +
|
||||||
|
|
@ -53,6 +58,9 @@ const NewsArticleWidget = () => {
|
||||||
`?id=${new URL(article.thumbnail).searchParams.get('id')}`
|
`?id=${new URL(article.thumbnail).searchParams.get('id')}`
|
||||||
}
|
}
|
||||||
alt={article.title}
|
alt={article.title}
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col justify-center flex-1 h-full pl-3 w-0">
|
<div className="flex flex-col justify-center flex-1 h-full pl-3 w-0">
|
||||||
<div className="font-bold text-xs text-black dark:text-white leading-tight truncate overflow-hidden whitespace-nowrap">
|
<div className="font-bold text-xs text-black dark:text-white leading-tight truncate overflow-hidden whitespace-nowrap">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import { ImagesIcon, PlusIcon } from 'lucide-react';
|
import { ImagesIcon, PlusIcon } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Lightbox from 'yet-another-react-lightbox';
|
import Lightbox from 'yet-another-react-lightbox';
|
||||||
import 'yet-another-react-lightbox/styles.css';
|
import 'yet-another-react-lightbox/styles.css';
|
||||||
import { Message } from './ChatWindow';
|
import { Message } from './ChatWindow';
|
||||||
|
|
@ -20,6 +21,7 @@ const SearchImages = ({
|
||||||
chatHistory: Message[];
|
chatHistory: Message[];
|
||||||
messageId: string;
|
messageId: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('components');
|
||||||
const [images, setImages] = useState<Image[] | null>(null);
|
const [images, setImages] = useState<Image[] | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
@ -75,7 +77,7 @@ const SearchImages = ({
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<ImagesIcon size={17} />
|
<ImagesIcon size={17} />
|
||||||
<p>Search images</p>
|
<p>{t('searchImages.searchButton')}</p>
|
||||||
</div>
|
</div>
|
||||||
<PlusIcon className="text-[#24A0ED]" size={17} />
|
<PlusIcon className="text-[#24A0ED]" size={17} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -142,7 +144,7 @@ const SearchImages = ({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-black/70 dark:text-white/70 text-xs">
|
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||||
View {images.length - 3} more
|
{t('common.viewMore', { count: images.length - 3 })}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useRef, useState } from 'react';
|
||||||
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
|
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
|
||||||
import 'yet-another-react-lightbox/styles.css';
|
import 'yet-another-react-lightbox/styles.css';
|
||||||
import { Message } from './ChatWindow';
|
import { Message } from './ChatWindow';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
type Video = {
|
type Video = {
|
||||||
url: string;
|
url: string;
|
||||||
|
|
@ -33,6 +34,7 @@ const Searchvideos = ({
|
||||||
chatHistory: Message[];
|
chatHistory: Message[];
|
||||||
messageId: string;
|
messageId: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const t = useTranslations('components');
|
||||||
const [videos, setVideos] = useState<Video[] | null>(null);
|
const [videos, setVideos] = useState<Video[] | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
@ -92,7 +94,7 @@ const Searchvideos = ({
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
<VideoIcon size={17} />
|
<VideoIcon size={17} />
|
||||||
<p>Search videos</p>
|
<p>{t('searchVideos.searchButton')}</p>
|
||||||
</div>
|
</div>
|
||||||
<PlusIcon className="text-[#24A0ED]" size={17} />
|
<PlusIcon className="text-[#24A0ED]" size={17} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -131,7 +133,7 @@ const Searchvideos = ({
|
||||||
/>
|
/>
|
||||||
<div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
|
<div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
|
||||||
<PlayCircle size={15} />
|
<PlayCircle size={15} />
|
||||||
<p className="text-xs">Video</p>
|
<p className="text-xs">{t('searchVideos.badge')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|
@ -155,7 +157,7 @@ const Searchvideos = ({
|
||||||
/>
|
/>
|
||||||
<div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
|
<div className="absolute bg-white/70 dark:bg-black/70 text-black/70 dark:text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
|
||||||
<PlayCircle size={15} />
|
<PlayCircle size={15} />
|
||||||
<p className="text-xs">Video</p>
|
<p className="text-xs">{t('searchVideos.badge')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -175,7 +177,7 @@ const Searchvideos = ({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-black/70 dark:text-white/70 text-xs">
|
<p className="text-black/70 dark:text-white/70 text-xs">
|
||||||
View {videos.length - 3} more
|
{t('common.viewMore', { count: videos.length - 3 })}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import Link from 'next/link';
|
||||||
import { useSelectedLayoutSegments } from 'next/navigation';
|
import { useSelectedLayoutSegments } from 'next/navigation';
|
||||||
import React, { useState, type ReactNode } from 'react';
|
import React, { useState, type ReactNode } from 'react';
|
||||||
import Layout from './Layout';
|
import Layout from './Layout';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const VerticalIconContainer = ({ children }: { children: ReactNode }) => {
|
const VerticalIconContainer = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -15,25 +16,26 @@ const VerticalIconContainer = ({ children }: { children: ReactNode }) => {
|
||||||
|
|
||||||
const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
||||||
const segments = useSelectedLayoutSegments();
|
const segments = useSelectedLayoutSegments();
|
||||||
|
const t = useTranslations('navigation');
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{
|
{
|
||||||
icon: Home,
|
icon: Home,
|
||||||
href: '/',
|
href: '/',
|
||||||
active: segments.length === 0 || segments.includes('c'),
|
active: segments.length === 0 || segments.includes('c'),
|
||||||
label: 'Home',
|
label: t('home'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Search,
|
icon: Search,
|
||||||
href: '/discover',
|
href: '/discover',
|
||||||
active: segments.includes('discover'),
|
active: segments.includes('discover'),
|
||||||
label: 'Discover',
|
label: t('discover'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: BookOpenText,
|
icon: BookOpenText,
|
||||||
href: '/library',
|
href: '/library',
|
||||||
active: segments.includes('library'),
|
active: segments.includes('library'),
|
||||||
label: 'Library',
|
label: t('library'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { Cloud, Sun, CloudRain, CloudSnow, Wind } from 'lucide-react';
|
import { Cloud, Sun, CloudRain, CloudSnow, Wind } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
|
||||||
const WeatherWidget = () => {
|
const WeatherWidget = () => {
|
||||||
|
const t = useTranslations('components');
|
||||||
|
const locale = useLocale();
|
||||||
const [data, setData] = useState({
|
const [data, setData] = useState({
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
condition: '',
|
condition: '',
|
||||||
|
|
@ -42,7 +46,7 @@ const WeatherWidget = () => {
|
||||||
if (result.state === 'granted') {
|
if (result.state === 'granted') {
|
||||||
navigator.geolocation.getCurrentPosition(async (position) => {
|
navigator.geolocation.getCurrentPosition(async (position) => {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://api-bdc.io/data/reverse-geocode-client?latitude=${position.coords.latitude}&longitude=${position.coords.longitude}&localityLanguage=en`,
|
`https://api-bdc.io/data/reverse-geocode-client?latitude=${position.coords.latitude}&longitude=${position.coords.longitude}&localityLanguage=${locale}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -100,7 +104,7 @@ const WeatherWidget = () => {
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [locale]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-light-secondary dark:bg-dark-secondary rounded-xl border border-light-200 dark:border-dark-200 shadow-sm flex flex-row items-center w-full h-24 min-h-[96px] max-h-[96px] px-3 py-2 gap-3">
|
<div className="bg-light-secondary dark:bg-dark-secondary rounded-xl border border-light-200 dark:border-dark-200 shadow-sm flex flex-row items-center w-full h-24 min-h-[96px] max-h-[96px] px-3 py-2 gap-3">
|
||||||
|
|
@ -125,9 +129,11 @@ const WeatherWidget = () => {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col items-center justify-center w-16 min-w-16 max-w-16 h-full">
|
<div className="flex flex-col items-center justify-center w-16 min-w-16 max-w-16 h-full">
|
||||||
<img
|
<Image
|
||||||
src={`/weather-ico/${data.icon}.svg`}
|
src={`/weather-ico/${data.icon}.svg`}
|
||||||
alt={data.condition}
|
alt={data.condition}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
className="h-10 w-auto"
|
className="h-10 w-auto"
|
||||||
/>
|
/>
|
||||||
<span className="text-base font-semibold text-black dark:text-white">
|
<span className="text-base font-semibold text-black dark:text-white">
|
||||||
|
|
@ -148,8 +154,10 @@ const WeatherWidget = () => {
|
||||||
{data.condition}
|
{data.condition}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row justify-between w-full mt-auto pt-1 border-t border-light-200 dark:border-dark-200 text-xs text-black/60 dark:text-white/60">
|
<div className="flex flex-row justify-between w-full mt-auto pt-1 border-t border-light-200 dark:border-dark-200 text-xs text-black/60 dark:text-white/60">
|
||||||
<span>Humidity: {data.humidity}%</span>
|
<span>
|
||||||
<span>Now</span>
|
{t('weather.humidity')}: {data.humidity}%
|
||||||
|
</span>
|
||||||
|
<span>{t('weather.now')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import Select from '../ui/Select';
|
import Select from '../ui/Select';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
type Theme = 'dark' | 'light' | 'system';
|
type Theme = 'dark' | 'light' | 'system';
|
||||||
|
|
||||||
const ThemeSwitcher = ({ className }: { className?: string }) => {
|
const ThemeSwitcher = ({ className }: { className?: string }) => {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const t = useTranslations('components');
|
||||||
|
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
|
@ -50,8 +52,8 @@ const ThemeSwitcher = ({ className }: { className?: string }) => {
|
||||||
value={theme}
|
value={theme}
|
||||||
onChange={(e) => handleThemeSwitch(e.target.value as Theme)}
|
onChange={(e) => handleThemeSwitch(e.target.value as Theme)}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'light', label: 'Light' },
|
{ value: 'light', label: t('themeSwitcher.options.light') },
|
||||||
{ value: 'dark', label: 'Dark' },
|
{ value: 'dark', label: t('themeSwitcher.options.dark') },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
40
src/i18n/locales.ts
Normal file
40
src/i18n/locales.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
export const LOCALES = [
|
||||||
|
'en-US',
|
||||||
|
'en-GB',
|
||||||
|
'zh-TW',
|
||||||
|
'zh-HK',
|
||||||
|
'zh-CN',
|
||||||
|
'ja',
|
||||||
|
'ko',
|
||||||
|
'fr-FR',
|
||||||
|
'fr-CA',
|
||||||
|
'de',
|
||||||
|
] as const;
|
||||||
|
export type AppLocale = (typeof LOCALES)[number];
|
||||||
|
|
||||||
|
// Default locale for fallbacks
|
||||||
|
export const DEFAULT_LOCALE: AppLocale = 'en-US';
|
||||||
|
|
||||||
|
// UI labels for language options
|
||||||
|
export const LOCALE_LABELS: Record<AppLocale, string> = {
|
||||||
|
'en-US': 'English (US)',
|
||||||
|
'en-GB': 'English (UK)',
|
||||||
|
'zh-TW': '繁體中文',
|
||||||
|
'zh-HK': '繁體中文(香港)',
|
||||||
|
'zh-CN': '简体中文',
|
||||||
|
ja: '日本語',
|
||||||
|
ko: '한국어',
|
||||||
|
'fr-FR': 'Français (France)',
|
||||||
|
'fr-CA': 'Français (Canada)',
|
||||||
|
de: 'Deutsch',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Human-readable language name for prompt prefix
|
||||||
|
export function getPromptLanguageName(loc: string): string {
|
||||||
|
const l = (loc || '').toLowerCase();
|
||||||
|
const match = (
|
||||||
|
Object.keys(LOCALE_LABELS) as Array<keyof typeof LOCALE_LABELS>
|
||||||
|
).find((k) => k.toLowerCase() === l);
|
||||||
|
if (match) return LOCALE_LABELS[match];
|
||||||
|
return LOCALE_LABELS[DEFAULT_LOCALE];
|
||||||
|
}
|
||||||
115
src/i18n/request.ts
Normal file
115
src/i18n/request.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { cookies, headers } from 'next/headers';
|
||||||
|
import { getRequestConfig } from 'next-intl/server';
|
||||||
|
import { LOCALES, DEFAULT_LOCALE, type AppLocale } from './locales';
|
||||||
|
|
||||||
|
export default getRequestConfig(async () => {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const rawCookieLocale = cookieStore.get('locale')?.value;
|
||||||
|
|
||||||
|
// Helper: parse Accept-Language and pick best supported locale
|
||||||
|
function resolveFromAcceptLanguage(al: string | null | undefined): AppLocale {
|
||||||
|
const supported = new Set<string>(LOCALES as readonly string[]);
|
||||||
|
const raw = (al || '').toLowerCase();
|
||||||
|
if (!raw) return DEFAULT_LOCALE;
|
||||||
|
|
||||||
|
type Candidate = { tag: string; q: number };
|
||||||
|
const candidates: Candidate[] = raw
|
||||||
|
.split(',')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => {
|
||||||
|
const [tagPart, ...params] = part.split(';');
|
||||||
|
const tag = tagPart.trim();
|
||||||
|
let q = 1;
|
||||||
|
for (const p of params) {
|
||||||
|
const m = p.trim().match(/^q=([0-9.]+)$/);
|
||||||
|
if (m) {
|
||||||
|
const v = parseFloat(m[1]);
|
||||||
|
if (!Number.isNaN(v)) q = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { tag, q } as Candidate;
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.q - a.q);
|
||||||
|
|
||||||
|
// Try in order: exact match -> base language match -> custom mapping
|
||||||
|
for (const { tag } of candidates) {
|
||||||
|
// exact match against supported
|
||||||
|
const exact = Array.from(supported).find((s) => s.toLowerCase() === tag);
|
||||||
|
if (exact) return exact as AppLocale;
|
||||||
|
|
||||||
|
// base language match (e.g., en-US -> en-GB/en-US: prefer en-US if available)
|
||||||
|
const base = tag.split('-')[0];
|
||||||
|
const englishVariants = Array.from(supported).filter((s) =>
|
||||||
|
s.toLowerCase().startsWith('en-'),
|
||||||
|
) as AppLocale[];
|
||||||
|
if (base === 'en' && englishVariants.length > 0) {
|
||||||
|
// prefer en-US as default English
|
||||||
|
const enUS = englishVariants.find((e) => e.toLowerCase() === 'en-us');
|
||||||
|
return (enUS || englishVariants[0]) as AppLocale;
|
||||||
|
}
|
||||||
|
const baseMatch = Array.from(supported).find(
|
||||||
|
(s) => s.split('-')[0].toLowerCase() === base,
|
||||||
|
);
|
||||||
|
if (baseMatch) return baseMatch as AppLocale;
|
||||||
|
|
||||||
|
// custom mapping for Chinese:
|
||||||
|
// - zh-HK -> zh-HK
|
||||||
|
// - zh-TW -> zh-TW
|
||||||
|
// - zh-CN, zh-SG -> zh-CN
|
||||||
|
if (tag.startsWith('zh')) {
|
||||||
|
if (/^zh-(hk)/i.test(tag)) return 'zh-HK';
|
||||||
|
if (/^zh-(tw)/i.test(tag)) return 'zh-TW';
|
||||||
|
if (/^zh-(cn|sg)/i.test(tag)) return 'zh-CN';
|
||||||
|
// default Chinese fallback: zh-TW
|
||||||
|
return 'zh-TW';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DEFAULT_LOCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize any incoming locale (including legacy cookies like 'en' or 'fr')
|
||||||
|
function normalizeToSupported(loc?: string | null): AppLocale | null {
|
||||||
|
const val = (loc || '').trim();
|
||||||
|
if (!val) return null;
|
||||||
|
const lower = val.toLowerCase();
|
||||||
|
// exact case-insensitive match against supported
|
||||||
|
const exact = (LOCALES as readonly string[]).find(
|
||||||
|
(s) => s.toLowerCase() === lower,
|
||||||
|
);
|
||||||
|
if (exact) return exact as AppLocale;
|
||||||
|
|
||||||
|
// map base tags to preferred regional variants
|
||||||
|
const base = lower.split('-')[0];
|
||||||
|
if (base === 'en') return 'en-US';
|
||||||
|
if (base === 'fr') return 'fr-FR';
|
||||||
|
if (base === 'zh') {
|
||||||
|
if (/^zh-(hk)/i.test(lower)) return 'zh-HK';
|
||||||
|
if (/^zh-(tw)/i.test(lower)) return 'zh-TW';
|
||||||
|
if (/^zh-(cn|sg)/i.test(lower)) return 'zh-CN';
|
||||||
|
// default Chinese fallback
|
||||||
|
return 'zh-TW';
|
||||||
|
}
|
||||||
|
// try base language match generically
|
||||||
|
const baseMatch = (LOCALES as readonly string[]).find(
|
||||||
|
(s) => s.split('-')[0].toLowerCase() === base,
|
||||||
|
);
|
||||||
|
return (baseMatch as AppLocale) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer normalized cookie if present and valid; otherwise use Accept-Language
|
||||||
|
let locale: AppLocale;
|
||||||
|
const normalizedCookie = normalizeToSupported(rawCookieLocale);
|
||||||
|
if (normalizedCookie) {
|
||||||
|
locale = normalizedCookie;
|
||||||
|
} else {
|
||||||
|
const hdrs = await headers();
|
||||||
|
const acceptLanguage = hdrs.get('accept-language');
|
||||||
|
locale = resolveFromAcceptLanguage(acceptLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
messages: (await import(`../../messages/${locale}.json`)).default,
|
||||||
|
};
|
||||||
|
});
|
||||||
45
src/lib/pdfFont.ts
Normal file
45
src/lib/pdfFont.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Runtime loader for a Unicode-capable font (Noto Sans TC) for jsPDF, to fix CJK garbled text
|
||||||
|
// Note: Load fonts from local /public/fonts to avoid any external CDN dependency.
|
||||||
|
// License: Noto fonts are under SIL Open Font License 1.1
|
||||||
|
|
||||||
|
import jsPDF from 'jspdf';
|
||||||
|
|
||||||
|
let fontsLoaded = false;
|
||||||
|
|
||||||
|
// Files should be placed at public/fonts (see public/fonts/README.md)
|
||||||
|
const NOTO_TC_REGULAR = '/fonts/NotoSansTC-Regular.ttf';
|
||||||
|
const NOTO_TC_BOLD = '/fonts/NotoSansTC-Bold.ttf';
|
||||||
|
|
||||||
|
function arrayBufferToBinaryString(buffer: ArrayBuffer): string {
|
||||||
|
let binary = '';
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const chunkSize = 0x8000; // avoid call stack overflow
|
||||||
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||||
|
const chunk = bytes.subarray(i, i + chunkSize);
|
||||||
|
binary += String.fromCharCode.apply(
|
||||||
|
null,
|
||||||
|
Array.from(chunk) as unknown as number[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureNotoSansTC(doc: jsPDF): Promise<void> {
|
||||||
|
if (fontsLoaded) return;
|
||||||
|
const [regBuf, boldBuf] = await Promise.all([
|
||||||
|
fetch(NOTO_TC_REGULAR).then((r) => r.arrayBuffer()),
|
||||||
|
fetch(NOTO_TC_BOLD).then((r) => r.arrayBuffer()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const regBin = arrayBufferToBinaryString(regBuf);
|
||||||
|
const boldBin = arrayBufferToBinaryString(boldBuf);
|
||||||
|
|
||||||
|
// Register into VFS and add fonts
|
||||||
|
doc.addFileToVFS('NotoSansTC-Regular.ttf', regBin);
|
||||||
|
doc.addFont('NotoSansTC-Regular.ttf', 'NotoSansTC', 'normal');
|
||||||
|
|
||||||
|
doc.addFileToVFS('NotoSansTC-Bold.ttf', boldBin);
|
||||||
|
doc.addFont('NotoSansTC-Bold.ttf', 'NotoSansTC', 'bold');
|
||||||
|
|
||||||
|
fontsLoaded = true;
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
youtubeSearchRetrieverPrompt,
|
youtubeSearchRetrieverPrompt,
|
||||||
} from './youtubeSearch';
|
} from './youtubeSearch';
|
||||||
|
|
||||||
export default {
|
const prompts = {
|
||||||
webSearchResponsePrompt,
|
webSearchResponsePrompt,
|
||||||
webSearchRetrieverPrompt,
|
webSearchRetrieverPrompt,
|
||||||
academicSearchResponsePrompt,
|
academicSearchResponsePrompt,
|
||||||
|
|
@ -30,3 +30,5 @@ export default {
|
||||||
youtubeSearchResponsePrompt,
|
youtubeSearchResponsePrompt,
|
||||||
youtubeSearchRetrieverPrompt,
|
youtubeSearchRetrieverPrompt,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default prompts;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import LineOutputParser from '../outputParsers/lineOutputParser';
|
||||||
import { getDocumentsFromLinks } from '../utils/documents';
|
import { getDocumentsFromLinks } from '../utils/documents';
|
||||||
import { Document } from 'langchain/document';
|
import { Document } from 'langchain/document';
|
||||||
import { searchSearxng } from '../searxng';
|
import { searchSearxng } from '../searxng';
|
||||||
|
import { getLocale } from 'next-intl/server';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import computeSimilarity from '../utils/computeSimilarity';
|
import computeSimilarity from '../utils/computeSimilarity';
|
||||||
|
|
@ -205,8 +206,10 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
||||||
} else {
|
} else {
|
||||||
question = question.replace(/<think>.*?<\/think>/g, '');
|
question = question.replace(/<think>.*?<\/think>/g, '');
|
||||||
|
|
||||||
|
const currentLocale = await getLocale();
|
||||||
|
const baseLang = (currentLocale?.split('-')[0] || 'en') as string;
|
||||||
const res = await searchSearxng(question, {
|
const res = await searchSearxng(question, {
|
||||||
language: 'en',
|
language: baseLang,
|
||||||
engines: this.config.activeEngines,
|
engines: this.config.activeEngines,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,22 @@ import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes));
|
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes));
|
||||||
|
|
||||||
|
// Locale-aware absolute date formatting
|
||||||
|
export const formatDate = (
|
||||||
|
date: Date | string,
|
||||||
|
locale: string,
|
||||||
|
options: Intl.DateTimeFormatOptions = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const d = new Date(date);
|
||||||
|
return new Intl.DateTimeFormat(locale || undefined, options).format(d);
|
||||||
|
};
|
||||||
|
|
||||||
export const formatTimeDifference = (
|
export const formatTimeDifference = (
|
||||||
date1: Date | string,
|
date1: Date | string,
|
||||||
date2: Date | string,
|
date2: Date | string,
|
||||||
|
|
@ -25,3 +41,45 @@ export const formatTimeDifference = (
|
||||||
else
|
else
|
||||||
return `${Math.floor(diffInSeconds / 31536000)} year${Math.floor(diffInSeconds / 31536000) !== 1 ? 's' : ''}`;
|
return `${Math.floor(diffInSeconds / 31536000)} year${Math.floor(diffInSeconds / 31536000) !== 1 ? 's' : ''}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Locale-aware relative time using Intl.RelativeTimeFormat
|
||||||
|
export const formatRelativeTime = (
|
||||||
|
date1: Date | string,
|
||||||
|
date2: Date | string,
|
||||||
|
locale: string,
|
||||||
|
): string => {
|
||||||
|
const d1 = new Date(date1);
|
||||||
|
const d2 = new Date(date2);
|
||||||
|
const diffSeconds = Math.floor((d2.getTime() - d1.getTime()) / 1000); // positive if d2 > d1
|
||||||
|
|
||||||
|
const abs = Math.abs(diffSeconds);
|
||||||
|
let value: number;
|
||||||
|
let unit: Intl.RelativeTimeFormatUnit;
|
||||||
|
|
||||||
|
if (abs < 60) {
|
||||||
|
value = Math.round(diffSeconds);
|
||||||
|
unit = 'second';
|
||||||
|
} else if (abs < 3600) {
|
||||||
|
value = Math.round(diffSeconds / 60);
|
||||||
|
unit = 'minute';
|
||||||
|
} else if (abs < 86400) {
|
||||||
|
value = Math.round(diffSeconds / 3600);
|
||||||
|
unit = 'hour';
|
||||||
|
} else if (abs < 2629800) {
|
||||||
|
// ~1 month (30.4 days)
|
||||||
|
value = Math.round(diffSeconds / 86400);
|
||||||
|
unit = 'day';
|
||||||
|
} else if (abs < 31557600) {
|
||||||
|
// ~1 year
|
||||||
|
value = Math.round(diffSeconds / 2629800);
|
||||||
|
unit = 'month';
|
||||||
|
} else {
|
||||||
|
value = Math.round(diffSeconds / 31557600);
|
||||||
|
unit = 'year';
|
||||||
|
}
|
||||||
|
|
||||||
|
const rtf = new Intl.RelativeTimeFormat(locale || undefined, {
|
||||||
|
numeric: 'auto',
|
||||||
|
});
|
||||||
|
return rtf.format(value, unit);
|
||||||
|
};
|
||||||
|
|
|
||||||
263
yarn.lock
263
yarn.lock
|
|
@ -369,6 +369,54 @@
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62"
|
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62"
|
||||||
integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==
|
integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==
|
||||||
|
|
||||||
|
"@formatjs/ecma402-abstract@2.3.4":
|
||||||
|
version "2.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz#e90c5a846ba2b33d92bc400fdd709da588280fbc"
|
||||||
|
integrity sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==
|
||||||
|
dependencies:
|
||||||
|
"@formatjs/fast-memoize" "2.2.7"
|
||||||
|
"@formatjs/intl-localematcher" "0.6.1"
|
||||||
|
decimal.js "^10.4.3"
|
||||||
|
tslib "^2.8.0"
|
||||||
|
|
||||||
|
"@formatjs/fast-memoize@2.2.7", "@formatjs/fast-memoize@^2.2.0":
|
||||||
|
version "2.2.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz#707f9ddaeb522a32f6715bb7950b0831f4cc7b15"
|
||||||
|
integrity sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.8.0"
|
||||||
|
|
||||||
|
"@formatjs/icu-messageformat-parser@2.11.2":
|
||||||
|
version "2.11.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz#85aea211bea40aa81ee1d44ac7accc3cf5500a73"
|
||||||
|
integrity sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==
|
||||||
|
dependencies:
|
||||||
|
"@formatjs/ecma402-abstract" "2.3.4"
|
||||||
|
"@formatjs/icu-skeleton-parser" "1.8.14"
|
||||||
|
tslib "^2.8.0"
|
||||||
|
|
||||||
|
"@formatjs/icu-skeleton-parser@1.8.14":
|
||||||
|
version "1.8.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz#b9581d00363908efb29817fdffc32b79f41dabe5"
|
||||||
|
integrity sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==
|
||||||
|
dependencies:
|
||||||
|
"@formatjs/ecma402-abstract" "2.3.4"
|
||||||
|
tslib "^2.8.0"
|
||||||
|
|
||||||
|
"@formatjs/intl-localematcher@0.6.1":
|
||||||
|
version "0.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz#25dc30675320bf65a9d7f73876fc1e4064c0e299"
|
||||||
|
integrity sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.8.0"
|
||||||
|
|
||||||
|
"@formatjs/intl-localematcher@^0.5.4":
|
||||||
|
version "0.5.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz#1e0bd3fc1332c1fe4540cfa28f07e9227b659a58"
|
||||||
|
integrity sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==
|
||||||
|
dependencies:
|
||||||
|
tslib "2"
|
||||||
|
|
||||||
"@google/generative-ai@^0.24.0":
|
"@google/generative-ai@^0.24.0":
|
||||||
version "0.24.1"
|
version "0.24.1"
|
||||||
resolved "https://registry.yarnpkg.com/@google/generative-ai/-/generative-ai-0.24.1.tgz#634a3c06f8ea7a6125c1b0d6c1e66bb11afb52c9"
|
resolved "https://registry.yarnpkg.com/@google/generative-ai/-/generative-ai-0.24.1.tgz#634a3c06f8ea7a6125c1b0d6c1e66bb11afb52c9"
|
||||||
|
|
@ -885,6 +933,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz#7ca168b6937818e9a74b47ac4e2112b2e1a024cf"
|
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz#7ca168b6937818e9a74b47ac4e2112b2e1a024cf"
|
||||||
integrity sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg==
|
integrity sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg==
|
||||||
|
|
||||||
|
"@schummar/icu-type-parser@1.21.5":
|
||||||
|
version "1.21.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz#75989085bbbf80ee325874a0137437bde77e9baf"
|
||||||
|
integrity sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==
|
||||||
|
|
||||||
"@selderee/plugin-htmlparser2@^0.11.0":
|
"@selderee/plugin-htmlparser2@^0.11.0":
|
||||||
version "0.11.0"
|
version "0.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz#d5b5e29a7ba6d3958a1972c7be16f4b2c188c517"
|
resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz#d5b5e29a7ba6d3958a1972c7be16f4b2c188c517"
|
||||||
|
|
@ -956,6 +1009,14 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
|
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
|
||||||
integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
|
integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
|
||||||
|
|
||||||
|
"@types/node-fetch@^2.6.4":
|
||||||
|
version "2.6.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.13.tgz#e0c9b7b5edbdb1b50ce32c127e85e880872d56ee"
|
||||||
|
integrity sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
form-data "^4.0.4"
|
||||||
|
|
||||||
"@types/node@*", "@types/node@^20":
|
"@types/node@*", "@types/node@^20":
|
||||||
version "20.12.5"
|
version "20.12.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3"
|
||||||
|
|
@ -970,6 +1031,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~6.20.0"
|
undici-types "~6.20.0"
|
||||||
|
|
||||||
|
"@types/node@^18.11.18":
|
||||||
|
version "18.19.123"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.123.tgz#08a3e4f5e0c73b8840c677b7635ce59d5dc1f76d"
|
||||||
|
integrity sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
"@types/pdf-parse@^1.1.4":
|
"@types/pdf-parse@^1.1.4":
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/pdf-parse/-/pdf-parse-1.1.4.tgz#21a539efd2f16009d08aeed3350133948b5d7ed1"
|
resolved "https://registry.yarnpkg.com/@types/pdf-parse/-/pdf-parse-1.1.4.tgz#21a539efd2f16009d08aeed3350133948b5d7ed1"
|
||||||
|
|
@ -1092,6 +1160,13 @@ abort-controller-x@^0.4.0, abort-controller-x@^0.4.3:
|
||||||
resolved "https://registry.yarnpkg.com/abort-controller-x/-/abort-controller-x-0.4.3.tgz#ff269788386fabd58a7b6eeaafcb6cf55c2958e0"
|
resolved "https://registry.yarnpkg.com/abort-controller-x/-/abort-controller-x-0.4.3.tgz#ff269788386fabd58a7b6eeaafcb6cf55c2958e0"
|
||||||
integrity sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==
|
integrity sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==
|
||||||
|
|
||||||
|
abort-controller@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
||||||
|
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
|
||||||
|
dependencies:
|
||||||
|
event-target-shim "^5.0.0"
|
||||||
|
|
||||||
acorn-jsx@^5.3.2:
|
acorn-jsx@^5.3.2:
|
||||||
version "5.3.2"
|
version "5.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||||
|
|
@ -1102,6 +1177,13 @@ acorn@^8.9.0:
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
|
||||||
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
|
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
|
||||||
|
|
||||||
|
agentkeepalive@^4.2.1:
|
||||||
|
version "4.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a"
|
||||||
|
integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==
|
||||||
|
dependencies:
|
||||||
|
humanize-ms "^1.2.1"
|
||||||
|
|
||||||
ajv@^6.12.4:
|
ajv@^6.12.4:
|
||||||
version "6.12.6"
|
version "6.12.6"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||||
|
|
@ -1484,6 +1566,14 @@ busboy@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
streamsearch "^1.1.0"
|
streamsearch "^1.1.0"
|
||||||
|
|
||||||
|
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
||||||
|
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
|
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
|
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
|
||||||
|
|
@ -1510,15 +1600,10 @@ camelcase@6:
|
||||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
||||||
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001579:
|
caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
|
||||||
version "1.0.30001705"
|
version "1.0.30001735"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001705.tgz#dc3510bcdef261444ca944b7be9c8d0bb7fafeef"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz"
|
||||||
integrity sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg==
|
integrity sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
|
|
||||||
version "1.0.30001606"
|
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz#b4d5f67ab0746a3b8b5b6d1f06e39c51beb39a9e"
|
|
||||||
integrity sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg==
|
|
||||||
|
|
||||||
canvg@^3.0.11:
|
canvg@^3.0.11:
|
||||||
version "3.0.11"
|
version "3.0.11"
|
||||||
|
|
@ -1786,6 +1871,11 @@ decamelize@1.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||||
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
||||||
|
|
||||||
|
decimal.js@^10.4.3:
|
||||||
|
version "10.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
|
||||||
|
integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
|
||||||
|
|
||||||
decompress-response@^6.0.0:
|
decompress-response@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
|
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
|
||||||
|
|
@ -1937,6 +2027,15 @@ duck@^0.1.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
underscore "^1.13.1"
|
underscore "^1.13.1"
|
||||||
|
|
||||||
|
dunder-proto@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
||||||
|
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers "^1.0.1"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
gopd "^1.2.0"
|
||||||
|
|
||||||
eastasianwidth@^0.2.0:
|
eastasianwidth@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
||||||
|
|
@ -2046,6 +2145,11 @@ es-define-property@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic "^1.2.4"
|
get-intrinsic "^1.2.4"
|
||||||
|
|
||||||
|
es-define-property@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
|
||||||
|
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||||
|
|
||||||
es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0:
|
es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||||
|
|
@ -2078,6 +2182,13 @@ es-object-atoms@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors "^1.3.0"
|
es-errors "^1.3.0"
|
||||||
|
|
||||||
|
es-object-atoms@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
||||||
|
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
|
||||||
es-set-tostringtag@^2.0.3:
|
es-set-tostringtag@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777"
|
resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777"
|
||||||
|
|
@ -2087,6 +2198,16 @@ es-set-tostringtag@^2.0.3:
|
||||||
has-tostringtag "^1.0.2"
|
has-tostringtag "^1.0.2"
|
||||||
hasown "^2.0.1"
|
hasown "^2.0.1"
|
||||||
|
|
||||||
|
es-set-tostringtag@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
|
||||||
|
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
get-intrinsic "^1.2.6"
|
||||||
|
has-tostringtag "^1.0.2"
|
||||||
|
hasown "^2.0.2"
|
||||||
|
|
||||||
es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2:
|
es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763"
|
resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763"
|
||||||
|
|
@ -2385,6 +2506,11 @@ esutils@^2.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||||
|
|
||||||
|
event-target-shim@^5.0.0:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||||
|
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||||
|
|
||||||
eventemitter3@^4.0.4:
|
eventemitter3@^4.0.4:
|
||||||
version "4.0.7"
|
version "4.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||||
|
|
@ -2531,6 +2657,11 @@ foreground-child@^3.1.0:
|
||||||
cross-spawn "^7.0.0"
|
cross-spawn "^7.0.0"
|
||||||
signal-exit "^4.0.1"
|
signal-exit "^4.0.1"
|
||||||
|
|
||||||
|
form-data-encoder@1.7.2:
|
||||||
|
version "1.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040"
|
||||||
|
integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==
|
||||||
|
|
||||||
form-data@^4.0.0:
|
form-data@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
||||||
|
|
@ -2540,6 +2671,25 @@ form-data@^4.0.0:
|
||||||
combined-stream "^1.0.8"
|
combined-stream "^1.0.8"
|
||||||
mime-types "^2.1.12"
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
form-data@^4.0.4:
|
||||||
|
version "4.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
|
||||||
|
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
|
||||||
|
dependencies:
|
||||||
|
asynckit "^0.4.0"
|
||||||
|
combined-stream "^1.0.8"
|
||||||
|
es-set-tostringtag "^2.1.0"
|
||||||
|
hasown "^2.0.2"
|
||||||
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
formdata-node@^4.3.2:
|
||||||
|
version "4.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2"
|
||||||
|
integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==
|
||||||
|
dependencies:
|
||||||
|
node-domexception "1.0.0"
|
||||||
|
web-streams-polyfill "4.0.0-beta.3"
|
||||||
|
|
||||||
fraction.js@^4.3.7:
|
fraction.js@^4.3.7:
|
||||||
version "4.3.7"
|
version "4.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||||
|
|
@ -2608,6 +2758,30 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@
|
||||||
has-symbols "^1.0.3"
|
has-symbols "^1.0.3"
|
||||||
hasown "^2.0.0"
|
hasown "^2.0.0"
|
||||||
|
|
||||||
|
get-intrinsic@^1.2.6:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||||
|
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers "^1.0.2"
|
||||||
|
es-define-property "^1.0.1"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
es-object-atoms "^1.1.1"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
get-proto "^1.0.1"
|
||||||
|
gopd "^1.2.0"
|
||||||
|
has-symbols "^1.1.0"
|
||||||
|
hasown "^2.0.2"
|
||||||
|
math-intrinsics "^1.1.0"
|
||||||
|
|
||||||
|
get-proto@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
||||||
|
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||||
|
dependencies:
|
||||||
|
dunder-proto "^1.0.1"
|
||||||
|
es-object-atoms "^1.0.0"
|
||||||
|
|
||||||
get-symbol-description@^1.0.2:
|
get-symbol-description@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5"
|
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5"
|
||||||
|
|
@ -2717,6 +2891,11 @@ gopd@^1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic "^1.1.3"
|
get-intrinsic "^1.1.3"
|
||||||
|
|
||||||
|
gopd@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
||||||
|
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||||
|
|
||||||
graceful-fs@^4.2.4:
|
graceful-fs@^4.2.4:
|
||||||
version "4.2.11"
|
version "4.2.11"
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||||
|
|
@ -2785,6 +2964,11 @@ has-symbols@^1.0.2, has-symbols@^1.0.3:
|
||||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
|
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
|
||||||
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
|
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
|
||||||
|
|
||||||
|
has-symbols@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
||||||
|
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||||
|
|
||||||
has-tostringtag@^1.0.0, has-tostringtag@^1.0.2:
|
has-tostringtag@^1.0.0, has-tostringtag@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
||||||
|
|
@ -2828,6 +3012,13 @@ htmlparser2@^8.0.2:
|
||||||
domutils "^3.0.1"
|
domutils "^3.0.1"
|
||||||
entities "^4.4.0"
|
entities "^4.4.0"
|
||||||
|
|
||||||
|
humanize-ms@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
|
||||||
|
integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
|
||||||
|
dependencies:
|
||||||
|
ms "^2.0.0"
|
||||||
|
|
||||||
ieee754@^1.1.13:
|
ieee754@^1.1.13:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||||
|
|
@ -2883,6 +3074,16 @@ internal-slot@^1.0.7:
|
||||||
hasown "^2.0.0"
|
hasown "^2.0.0"
|
||||||
side-channel "^1.0.4"
|
side-channel "^1.0.4"
|
||||||
|
|
||||||
|
intl-messageformat@^10.5.14:
|
||||||
|
version "10.7.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.16.tgz#d909f9f9f4ab857fbe681d559b958dd4dd9f665a"
|
||||||
|
integrity sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==
|
||||||
|
dependencies:
|
||||||
|
"@formatjs/ecma402-abstract" "2.3.4"
|
||||||
|
"@formatjs/fast-memoize" "2.2.7"
|
||||||
|
"@formatjs/icu-messageformat-parser" "2.11.2"
|
||||||
|
tslib "^2.8.0"
|
||||||
|
|
||||||
is-array-buffer@^3.0.4:
|
is-array-buffer@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98"
|
resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98"
|
||||||
|
|
@ -3393,6 +3594,11 @@ markdown-to-jsx@^7.7.2:
|
||||||
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.7.2.tgz#59c1dd64f48b53719311ab140be3cd51cdabccd3"
|
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.7.2.tgz#59c1dd64f48b53719311ab140be3cd51cdabccd3"
|
||||||
integrity sha512-N3AKfYRvxNscvcIH6HDnDKILp4S8UWbebp+s92Y8SwIq0CuSbLW4Jgmrbjku3CWKjTQO0OyIMS6AhzqrwjEa3g==
|
integrity sha512-N3AKfYRvxNscvcIH6HDnDKILp4S8UWbebp+s92Y8SwIq0CuSbLW4Jgmrbjku3CWKjTQO0OyIMS6AhzqrwjEa3g==
|
||||||
|
|
||||||
|
math-intrinsics@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||||
|
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||||
|
|
||||||
merge2@^1.3.0, merge2@^1.4.1:
|
merge2@^1.3.0, merge2@^1.4.1:
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||||
|
|
@ -3464,7 +3670,7 @@ ms@2.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||||
|
|
||||||
ms@^2.1.1:
|
ms@^2.0.0, ms@^2.1.1:
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||||
|
|
@ -3503,6 +3709,20 @@ natural-compare@^1.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||||
|
|
||||||
|
negotiator@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
|
||||||
|
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
|
||||||
|
|
||||||
|
next-intl@^4.3.4:
|
||||||
|
version "4.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/next-intl/-/next-intl-4.3.4.tgz#247ae789f1490b6518e8b18fe13d44cce703098b"
|
||||||
|
integrity sha512-VWLIDlGbnL/o4LnveJTJD1NOYN8lh3ZAGTWw2krhfgg53as3VsS4jzUVnArJdqvwtlpU/2BIDbWTZ7V4o1jFEw==
|
||||||
|
dependencies:
|
||||||
|
"@formatjs/intl-localematcher" "^0.5.4"
|
||||||
|
negotiator "^1.0.0"
|
||||||
|
use-intl "^4.3.4"
|
||||||
|
|
||||||
next-themes@^0.3.0:
|
next-themes@^0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a"
|
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a"
|
||||||
|
|
@ -3567,12 +3787,17 @@ node-addon-api@^6.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76"
|
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76"
|
||||||
integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==
|
integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==
|
||||||
|
|
||||||
|
node-domexception@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||||
|
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
||||||
|
|
||||||
node-ensure@^0.0.0:
|
node-ensure@^0.0.0:
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7"
|
resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7"
|
||||||
integrity sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==
|
integrity sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==
|
||||||
|
|
||||||
node-fetch@^2.7.0:
|
node-fetch@^2.6.7, node-fetch@^2.7.0:
|
||||||
version "2.7.0"
|
version "2.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||||
|
|
@ -4833,7 +5058,7 @@ tsconfig-paths@^3.15.0:
|
||||||
minimist "^1.2.6"
|
minimist "^1.2.6"
|
||||||
strip-bom "^3.0.0"
|
strip-bom "^3.0.0"
|
||||||
|
|
||||||
tslib@^2.4.0, tslib@^2.8.0:
|
tslib@2, tslib@^2.4.0, tslib@^2.8.0:
|
||||||
version "2.8.1"
|
version "2.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||||
|
|
@ -4951,6 +5176,15 @@ use-composed-ref@^1.3.0:
|
||||||
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
|
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
|
||||||
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
|
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
|
||||||
|
|
||||||
|
use-intl@^4.3.4:
|
||||||
|
version "4.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-intl/-/use-intl-4.3.4.tgz#42bb2141480032623ffe412799c62cbdf306ed0f"
|
||||||
|
integrity sha512-sHfiU0QeJ1rirNWRxvCyvlSh9+NczcOzRnPyMeo2rtHXhVnBsvMRjE+UG4eh3lRhCxrvcqei/I0lBxsc59on1w==
|
||||||
|
dependencies:
|
||||||
|
"@formatjs/fast-memoize" "^2.2.0"
|
||||||
|
"@schummar/icu-type-parser" "1.21.5"
|
||||||
|
intl-messageformat "^10.5.14"
|
||||||
|
|
||||||
use-isomorphic-layout-effect@^1.1.1:
|
use-isomorphic-layout-effect@^1.1.1:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
|
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
|
||||||
|
|
@ -5014,6 +5248,11 @@ weaviate-client@^3.5.2:
|
||||||
nice-grpc-common "^2.0.2"
|
nice-grpc-common "^2.0.2"
|
||||||
uuid "^9.0.1"
|
uuid "^9.0.1"
|
||||||
|
|
||||||
|
web-streams-polyfill@4.0.0-beta.3:
|
||||||
|
version "4.0.0-beta.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38"
|
||||||
|
integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==
|
||||||
|
|
||||||
webidl-conversions@^3.0.0:
|
webidl-conversions@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue