Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/app/[lang]/all-tools/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ export default async function AllToolsPage({ params }: { params: Promise<{ lang:
activeFilters: requireTranslationValue(t.common.active_filters, "common.active_filters"),
allFamilies: requireTranslationValue(t.common.all_families, "common.all_families"),
clearFilters: requireTranslationValue(t.common.clear_filters, "common.clear_filters"),
clearFavorites: requireTranslationValue(t.common.clear_favorites, "common.clear_favorites"),
commonWorkflows: requireTranslationValue(t.common.common_workflows, "common.common_workflows"),
favorites: requireTranslationValue(t.common.favorites, "common.favorites"),
filterByCategory: requireTranslationValue(t.common.filter_by_category, "common.filter_by_category"),
filterByExecution: requireTranslationValue(t.common.filter_by_execution, "common.filter_by_execution"),
filterByInputType: requireTranslationValue(t.common.filter_by_input_type, "common.filter_by_input_type"),
Expand All @@ -126,13 +128,19 @@ export default async function AllToolsPage({ params }: { params: Promise<{ lang:
inputUrlDomain: requireTranslationValue(t.common.input_url_domain, "common.input_url_domain"),
noResults: requireTranslationValue(t.common.no_results, "common.no_results"),
noResultsSuggestion: requireTranslationValue(t.common.no_results_suggestion, "common.no_results_suggestion"),
noFavorites: requireTranslationValue(t.common.no_favorites, "common.no_favorites"),
noRecentTools: requireTranslationValue(t.common.no_recent_tools, "common.no_recent_tools"),
open: requireTranslationValue(t.common.open, "common.open"),
popularTags: requireTranslationValue(t.common.popular_tags, "common.popular_tags"),
removeFilter: requireTranslationValue(t.common.remove_filter, "common.remove_filter"),
showFilters: requireTranslationValue(t.common.show_filters, "common.show_filters"),
closeFilters: requireTranslationValue(t.common.close_filters, "common.close_filters"),
clearRecentTools: requireTranslationValue(t.common.clear_recent_tools, "common.clear_recent_tools"),
recentTools: requireTranslationValue(t.common.recent_tools, "common.recent_tools"),
recentToolsPrivacy: requireTranslationValue(t.common.recent_tools_local_only, "common.recent_tools_local_only"),
addFavorite: requireTranslationValue(t.common.add_favorite, "common.add_favorite"),
removeFavorite: requireTranslationValue(t.common.remove_favorite, "common.remove_favorite"),
favoritesPrivacy: requireTranslationValue(t.common.favorites_local_only, "common.favorites_local_only"),
searchPlaceholder: requireTranslationValue(t.nav.search, "nav.search"),
toolsLabel: requireTranslationValue(t.common.tools, "common.tools"),
useCaseEncode: requireTranslationValue(t.common.use_case_encode, "common.use_case_encode"),
Expand Down
41 changes: 32 additions & 9 deletions src/components/layout/command-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import { useLang } from "@/core/i18n/lang-provider"
import { getCommandSearchToolByKey } from "@/generated/command-search-index"
import { readFavoriteToolKeys, readRecentToolKeys, TOOL_DISCOVERY_UPDATED_EVENT } from "@/core/storage/tool-discovery-state"
import { useSystemCommands } from "@/core/commands/registry"
import { scoreCommandSearch } from "@/core/search/command-search"
import { applyToolSearchScoreBonuses, scoreCommandSearch } from "@/core/search/command-search"
import {
getToolSearchMetadata,
getToolSearchMetadataTerms,
} from "@/core/search/tool-search-metadata"
import { trackSearchPerformed } from "@/core/analytics/analytics"
import { getAllToolsHref } from "@/core/routing/all-tools-route"
import { cn } from "@/core/utils/utils"
Expand Down Expand Up @@ -55,6 +59,7 @@ const WORKFLOW_ENTRIES = [
] as const

const RECOMMENDED_TOOL_KEYS = ["json_formatter", "jwt_decoder", "base64_encode_decode", "regex_tester"] as const
const TOOL_KEY_SCORE_MARKER = "tool-key:"

const CAPABILITY_LABEL_KEYS: Record<string, string> = {
"browser-local": "capability_browser_local",
Expand All @@ -79,6 +84,7 @@ type ToolCommandItem = {
searchValue: string
searchKeywords: string[]
capabilities: readonly string[]
popularity: number
}

type CommandPaletteProps = {
Expand All @@ -100,8 +106,9 @@ function isEditableShortcutTarget(target: EventTarget | null): boolean {
return Boolean(target.closest("input, textarea, select, [role='textbox'], .monaco-editor, .monaco-diff-editor"))
}

function buildToolSearchKeywords(toolKey: string, title: string, searchValue: string): string[] {
function buildToolSearchKeywords(toolKey: string, title: string, searchValue: string, locale?: string): string[] {
const tool = getCommandSearchToolByKey(toolKey)
const metadata = getToolSearchMetadata(toolKey)
return [
title,
searchValue,
Expand All @@ -110,6 +117,10 @@ function buildToolSearchKeywords(toolKey: string, title: string, searchValue: st
...(tool?.keywords ?? []),
...(tool?.aliases ?? []),
...(tool?.searchKeywords ?? []),
...(metadata.aliases ?? []),
...(metadata.taskSynonyms ?? []),
...(metadata.categoryTerms ?? []),
...getToolSearchMetadataTerms(toolKey, locale),
tool?.family,
...(tool?.tags ?? []),
...(tool?.capabilities ?? []),
Expand Down Expand Up @@ -280,6 +291,7 @@ export function CommandPalette({ open: openProp, onOpenChange, enableShortcut =
...(tool?.keywords ?? []),
...(tool?.aliases ?? []),
...(tool?.searchKeywords ?? []),
...getToolSearchMetadataTerms(item.key, lang),
tool?.family,
...(tool?.tags ?? []),
...(tool?.capabilities ?? []),
Expand All @@ -288,7 +300,7 @@ export function CommandPalette({ open: openProp, onOpenChange, enableShortcut =
}
}
return values
}, [englishToolSearchAliases, t.tools])
}, [englishToolSearchAliases, lang, t.tools])

const buildToolCommand = React.useCallback((toolKey: string): ToolCommandItem | null => {
const tool = getCommandSearchToolByKey(toolKey)
Expand All @@ -300,8 +312,9 @@ export function CommandPalette({ open: openProp, onOpenChange, enableShortcut =
title,
href: `/${lang}/${tool.slug}`,
searchValue,
searchKeywords: buildToolSearchKeywords(tool.key, title, searchValue),
searchKeywords: buildToolSearchKeywords(tool.key, title, searchValue, lang),
capabilities: tool.capabilities,
popularity: getToolSearchMetadata(tool.key).popularity ?? 0,
}
}, [lang, t.tools, toolSearchValues])

Expand Down Expand Up @@ -368,7 +381,16 @@ export function CommandPalette({ open: openProp, onOpenChange, enableShortcut =
onOpenChange={setOpen}
title={navigationLabel}
description={searchLabel}
filter={scoreCommandSearch}
filter={(value, search, keywords) => {
const safeKeywords = keywords?.filter((keyword) => !keyword.startsWith(TOOL_KEY_SCORE_MARKER))
const score = scoreCommandSearch(value, search, safeKeywords)
const toolKey = keywords?.find((keyword) => keyword.startsWith(TOOL_KEY_SCORE_MARKER))?.slice(TOOL_KEY_SCORE_MARKER.length)
if (!toolKey) return score
return applyToolSearchScoreBonuses(toolKey, score, {
favorite: favoriteToolKeys.includes(toolKey),
recentRank: recentToolKeys.indexOf(toolKey),
})
}}
>
<DialogTitle className="sr-only">{navigationLabel}</DialogTitle>
<CommandInput
Expand Down Expand Up @@ -438,7 +460,7 @@ export function CommandPalette({ open: openProp, onOpenChange, enableShortcut =
<CommandItem
key={`favorite-${item.toolKey}`}
value={item.searchValue}
keywords={item.searchKeywords}
keywords={[`${TOOL_KEY_SCORE_MARKER}${item.toolKey}`, ...item.searchKeywords]}
onSelect={() => runCommand(() => router.push(item.href))}
>
<ToolCommandRow item={item} labels={commonLabels as unknown as Record<string, string>} icon={Star} />
Expand All @@ -456,7 +478,7 @@ export function CommandPalette({ open: openProp, onOpenChange, enableShortcut =
<CommandItem
key={`recent-${item.toolKey}`}
value={item.searchValue}
keywords={item.searchKeywords}
keywords={[`${TOOL_KEY_SCORE_MARKER}${item.toolKey}`, ...item.searchKeywords]}
onSelect={() => runCommand(() => router.push(item.href))}
>
<ToolCommandRow item={item} labels={commonLabels as unknown as Record<string, string>} icon={History} />
Expand Down Expand Up @@ -547,7 +569,7 @@ export function CommandPalette({ open: openProp, onOpenChange, enableShortcut =
<CommandItem
key={tool.href}
value={toolSearchValues.get(tool.key) || title}
keywords={buildToolSearchKeywords(tool.key, title, toolSearchValues.get(tool.key) || title)}
keywords={[`${TOOL_KEY_SCORE_MARKER}${tool.key}`, ...buildToolSearchKeywords(tool.key, title, toolSearchValues.get(tool.key) || title, lang)]}
onSelect={() => runCommand(() => router.push(`/${lang}${tool.href}`))}
className={cn("items-start")}
>
Expand All @@ -557,8 +579,9 @@ export function CommandPalette({ open: openProp, onOpenChange, enableShortcut =
title,
href: `/${lang}${tool.href}`,
searchValue: toolSearchValues.get(tool.key) || title,
searchKeywords: buildToolSearchKeywords(tool.key, title, toolSearchValues.get(tool.key) || title),
searchKeywords: buildToolSearchKeywords(tool.key, title, toolSearchValues.get(tool.key) || title, lang),
capabilities: getCommandSearchToolByKey(tool.key)?.capabilities ?? [],
popularity: getToolSearchMetadata(tool.key).popularity ?? 0,
}}
labels={commonLabels as unknown as Record<string, string>}
icon={Sparkles}
Expand Down
22 changes: 13 additions & 9 deletions src/components/layout/route-page-chrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getRouteContext } from "@/core/routing/route-context"
import { recordRecentToolKey } from "@/core/storage/tool-discovery-state"
import { getRouteToolBySlug } from "@/generated/route-tool-lookup"
import { ToolTrustHeader } from "@/features/tool-shell/tool-trust-header"
import { ToolFavoriteControl } from "./tool-favorite-control"

const EXCLUDED_CONTENT_INTRO_SLUGS = new Set(["about", "pricing", "contact", "privacy", "terms", "install-app"])

Expand Down Expand Up @@ -75,15 +76,18 @@ function RoutePageChromeContent({ children, pathname }: RoutePageChromeProps) {
</div>
) : null}
{activeTool ? (
<ToolTrustHeader
slug={activeTool.slug}
sourceUrl={activeTool.sourceUrl}
privacy={activeTool.privacy}
networkAccess={activeTool.networkAccess}
networkHosts={activeTool.networkHosts}
networkPurposeKey={activeTool.networkPurposeKey}
externalDataSent={activeTool.externalDataSent}
/>
<>
<ToolFavoriteControl toolKey={activeTool.key} />
<ToolTrustHeader
slug={activeTool.slug}
sourceUrl={activeTool.sourceUrl}
privacy={activeTool.privacy}
networkAccess={activeTool.networkAccess}
networkHosts={activeTool.networkHosts}
networkPurposeKey={activeTool.networkPurposeKey}
externalDataSent={activeTool.externalDataSent}
/>
</>
) : null}
{children}
{routeContext.routeType === "tool" ? (
Expand Down
51 changes: 51 additions & 0 deletions src/components/layout/tool-favorite-control.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client"

import * as React from "react"
import { Heart } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useLang } from "@/core/i18n/lang-provider"
import { requireTranslationValue } from "@/core/i18n/i18n"
import {
readFavoriteToolKeys,
toggleFavoriteToolKey,
TOOL_DISCOVERY_UPDATED_EVENT,
} from "@/core/storage/tool-discovery-state"
import { cn } from "@/core/utils/utils"

type ToolFavoriteControlProps = {
toolKey: string
}

export function ToolFavoriteControl({ toolKey }: ToolFavoriteControlProps) {
const { t } = useLang()
const [favoriteToolKeys, setFavoriteToolKeys] = React.useState<string[]>([])
const isFavorite = favoriteToolKeys.includes(toolKey)
const addLabel = requireTranslationValue(t.common.add_favorite, "common.add_favorite")
const removeLabel = requireTranslationValue(t.common.remove_favorite, "common.remove_favorite")
const privacyLabel = requireTranslationValue(t.common.favorites_local_only, "common.favorites_local_only")

React.useEffect(() => {
const sync = () => setFavoriteToolKeys(readFavoriteToolKeys())
sync()
window.addEventListener(TOOL_DISCOVERY_UPDATED_EVENT, sync)
return () => window.removeEventListener(TOOL_DISCOVERY_UPDATED_EVENT, sync)
}, [])

return (
<div className="mb-4 flex flex-wrap items-center justify-end gap-2">
<Button
type="button"
variant="outline"
size="sm"
className="min-h-10"
aria-pressed={isFavorite}
onClick={() => setFavoriteToolKeys(toggleFavoriteToolKey(toolKey))}
>
<Heart className={cn("h-4 w-4", isFavorite ? "fill-primary text-primary" : "")} />
{isFavorite ? removeLabel : addLabel}
</Button>
<span className="text-xs text-muted-foreground">{privacyLabel}</span>
</div>
)
}

9 changes: 6 additions & 3 deletions src/core/i18n/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,10 @@
"sensitive_warning_privacy_policy": "Datenschutzrichtlinie",
"sensitive_warning_verify_devtools": "In DevTools prüfen",
"reveal_secret": "Geheimnis anzeigen",
"hide_secret": "Geheimnis ausblenden"
"hide_secret": "Geheimnis ausblenden",
"close_filters": "Fertig",
"clear_favorites": "Favoriten löschen",
"favorites_local_only": "Favorisierte Tool-IDs werden nur in diesem Browser gespeichert."
},
"tools": {
"json_formatter": {
Expand Down Expand Up @@ -2833,7 +2836,7 @@
"privacy_no_collection_title": "Umgang mit Tool-Eingaben",
"privacy_no_collection_desc": "Die meisten Workflows von byteflow.tools verarbeiten Eingaben und Ausgaben im Browser. Wir erfassen, speichern, analysieren oder laden Payloads browser-lokaler Tools nicht hoch. Tools mit externer Anfrage kontaktieren das angegebene Netzwerkziel nur, wenn Sie diese Aktion ausdrücklich ausführen.",
"privacy_cookies_title": "Cookies & Speicherung",
"privacy_cookies_desc": "localStorage wird nur für sichere UI-Einstellungen wie Theme, Sprache, Sidebar-Status und lokale Tool-Optionen verwendet. Sensible Payloads, Tokens, Logs, Dateiinhalte und Tool-Ausgaben sind nicht zur Speicherung vorgesehen.",
"privacy_cookies_desc": "localStorage wird nur für sichere UI-Einstellungen wie Theme, Sprache, Sidebar-Status, Favoriten-Tool-IDs, zuletzt genutzte Tool-IDs, Zeitstempel und lokale Tool-Optionen verwendet. Sensible Payloads, Tokens, Logs, Dateiinhalte, vollständige URLs und Tool-Ausgaben sind nicht zur Speicherung vorgesehen.",
"privacy_analytics_title": "Analyse",
"privacy_analytics_desc": "Analytics sind auf eine Cookie-freie Ereignistaxonomie mit Positivliste für aggregierte Produktsignale begrenzt. Erlaubte Ereignisse sind Tool geladen, Tool-Aktion, Kopieren, Download, Suche ausgeführt, Klick auf verwandtes Tool und PWA installiert. Suchereignisse erfassen nur Längen-Buckets und Ergebniszahlen. Tool-Eingaben, Ausgaben, JWTs, Geheimnisse, Logs, Dateinamen, Dateiinhalte, Bildinhalte, Hashwerte, vollständige URLs, Nutzer-IDs und Sitzungs-IDs sind niemals Analytics-Felder.",
"privacy_third_party_title": "Drittanbieter-Dienste",
Expand Down Expand Up @@ -2887,7 +2890,7 @@
"trust_center_external_tool_col_purpose": "Zweck",
"trust_center_external_tool_col_data": "Gesendete Daten",
"trust_center_storage_title": "Strategie für lokalen Speicher",
"trust_center_storage_desc": "Lokaler Speicher ist für sichere Einstellungen wie Theme, Sprache, Sidebar-Status, zuletzt genutzte Tool-IDs und lokale Presets reserviert. Tool-Payloads, Token, Logs, Dateiinhalte und erzeugte Ausgaben dürfen standardmäßig nicht persistiert werden.",
"trust_center_storage_desc": "Lokaler Speicher ist für sichere Einstellungen wie Theme, Sprache, Sidebar-Status, Favoriten-Tool-IDs, zuletzt genutzte Tool-IDs mit Zeitstempeln und lokale Presets reserviert. Tool-Payloads, Token, Logs, Dateiinhalte, vollständige URLs und erzeugte Ausgaben dürfen standardmäßig nicht persistiert werden.",
"trust_center_analytics_title": "Analytics-Strategie",
"trust_center_analytics_desc": "Analytics verwenden eine Ereignistaxonomie mit Positivliste und sind im Laufzeitcode standardmäßig deaktiviert. Wenn ein Cookie-freier Aggregationsanbieter aktiviert wird, darf er nur sichere Ereignisnamen und grobe Parameter wie Tool-ID, Aktionstyp, Sprache, Größen-Bucket, Ergebniszahl und Quellseite erhalten. Tool-Eingaben, Ausgaben, JWTs, Geheimnisse, Logtexte, Dateinamen, Dateiinhalte, Bildinhalte, Hashwerte, Suchtexte, vollständige URLs, Nutzer-IDs und Sitzungs-IDs sind verboten.",
"trust_center_pwa_title": "PWA-Cache-Strategie",
Expand Down
Loading