diff --git a/website/client/components/Home/TryIt.vue b/website/client/components/Home/TryIt.vue index c4d15ed7b..4c300ba8b 100644 --- a/website/client/components/Home/TryIt.vue +++ b/website/client/components/Home/TryIt.vue @@ -54,6 +54,22 @@ :loading="loading" :isValid="isSubmitValid" /> +
+ +
+ Reset all options to default values +
+
+
@@ -68,6 +84,7 @@ v-model:show-line-numbers="packOptions.showLineNumbers" v-model:output-parsable="packOptions.outputParsable" v-model:compress="packOptions.compress" + />
@@ -83,9 +100,10 @@ @@ -248,6 +311,29 @@ onMounted(() => { align-items: stretch; align-self: start; flex-shrink: 0; + gap: 8px; +} + +.reset-button { + display: flex; + align-items: center; + justify-content: center; + width: 50px; + height: 50px; + background: white; + color: var(--vp-c-text-2); + border: 1px solid var(--vp-c-border); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.reset-button:hover { + color: var(--vp-c-brand-1); + border-color: var(--vp-c-brand-1); + background: var(--vp-c-bg-soft); + } /* Responsive adjustments */ @@ -263,6 +349,45 @@ onMounted(() => { .pack-button-wrapper { width: 100%; + gap: 8px; } } +.tooltip-container { + position: relative; + display: inline-block; +} + +.tooltip-content { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + margin-bottom: 8px; + padding: 8px 12px; + background: #333; + color: white; + font-size: 0.875rem; + white-space: nowrap; + border-radius: 4px; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s, visibility 0.2s; + z-index: 10; +} + +.tooltip-container:hover .tooltip-content { + opacity: 1; + visibility: visible; +} + +.tooltip-arrow { + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border-width: 8px; + border-style: solid; + border-color: #333 transparent transparent transparent; +} + diff --git a/website/client/components/Home/TryItPackOptions.vue b/website/client/components/Home/TryItPackOptions.vue index 9795ed8b3..dfebb2403 100644 --- a/website/client/components/Home/TryItPackOptions.vue +++ b/website/client/components/Home/TryItPackOptions.vue @@ -144,6 +144,7 @@ function handleCompressToggle(enabled: boolean) {
+

Output Format Options

@@ -436,4 +437,5 @@ function handleCompressToggle(enabled: boolean) { outline: none; border-color: var(--vp-c-brand-1); } + diff --git a/website/client/composables/usePackOptions.ts b/website/client/composables/usePackOptions.ts index c8bc9d195..59c6edf65 100644 --- a/website/client/composables/usePackOptions.ts +++ b/website/client/composables/usePackOptions.ts @@ -1,4 +1,5 @@ import { computed, reactive } from 'vue'; +import { parseUrlParameters } from '../utils/urlParams'; export interface PackOptions { format: 'xml' | 'markdown' | 'plain'; @@ -27,7 +28,19 @@ const DEFAULT_PACK_OPTIONS: PackOptions = { }; export function usePackOptions() { - const packOptions = reactive({ ...DEFAULT_PACK_OPTIONS }); + // Initialize with URL parameters if available + const urlParams = parseUrlParameters(); + const initialOptions = { ...DEFAULT_PACK_OPTIONS }; + + // Apply URL parameters to initial options + for (const key of Object.keys(initialOptions) as Array) { + if (key in urlParams && urlParams[key] !== undefined) { + // Type-safe assignment: only assign if the key is a valid PackOptions key + initialOptions[key] = urlParams[key] as PackOptions[typeof key]; + } + } + + const packOptions = reactive(initialOptions); const getPackRequestOptions = computed(() => ({ removeComments: packOptions.removeComments, @@ -54,5 +67,6 @@ export function usePackOptions() { getPackRequestOptions, updateOption, resetOptions, + DEFAULT_PACK_OPTIONS, }; } diff --git a/website/client/composables/usePackRequest.ts b/website/client/composables/usePackRequest.ts index 242906837..6283cdca7 100644 --- a/website/client/composables/usePackRequest.ts +++ b/website/client/composables/usePackRequest.ts @@ -2,16 +2,20 @@ import { computed, ref } from 'vue'; import type { PackResult } from '../components/api/client'; import { handlePackRequest } from '../components/utils/requestHandlers'; import { isValidRemoteValue } from '../components/utils/validation'; +import { parseUrlParameters } from '../utils/urlParams'; import { usePackOptions } from './usePackOptions'; export type InputMode = 'url' | 'file' | 'folder'; export function usePackRequest() { const packOptionsComposable = usePackOptions(); - const { packOptions, getPackRequestOptions } = packOptionsComposable; + const { packOptions, getPackRequestOptions, resetOptions, DEFAULT_PACK_OPTIONS } = packOptionsComposable; + + // Initialize with URL parameters if available + const urlParams = parseUrlParameters(); // Input states - const inputUrl = ref(''); + const inputUrl = ref(urlParams.repo || ''); const inputRepositoryUrl = ref(''); const mode = ref('url'); const uploadedFile = ref(null); @@ -130,5 +134,9 @@ export function usePackRequest() { resetRequest, submitRequest, cancelRequest, + + // Pack option actions + resetOptions, + DEFAULT_PACK_OPTIONS, }; } diff --git a/website/client/utils/urlParams.ts b/website/client/utils/urlParams.ts new file mode 100644 index 000000000..6649738e2 --- /dev/null +++ b/website/client/utils/urlParams.ts @@ -0,0 +1,206 @@ +import type { PackOptions } from '../composables/usePackOptions'; + +// URL parameter constants +const BOOLEAN_PARAMS = [ + 'removeComments', + 'removeEmptyLines', + 'showLineNumbers', + 'fileSummary', + 'directoryStructure', + 'outputParsable', + 'compress', +] as const; + +const VALID_FORMATS = ['xml', 'markdown', 'plain'] as const; + +const URL_PARAM_KEYS = ['repo', 'format', 'style', 'include', 'ignore', ...BOOLEAN_PARAMS] as const; + +// Key mapping for internal names to URL parameter names +const KEY_MAPPING: Record = { + includePatterns: 'include', + ignorePatterns: 'ignore', +}; + +// Helper function to get URL parameter key from internal key +function getUrlParamKey(internalKey: string): string { + return KEY_MAPPING[internalKey] || internalKey; +} + +// Helper function to validate URL parameter values +export function validateUrlParameters(params: Record): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + // Validate format parameter + if (params.format && !(VALID_FORMATS as readonly string[]).includes(params.format as string)) { + errors.push(`Invalid format: ${params.format}. Must be one of: ${VALID_FORMATS.join(', ')}`); + } + + // Validate URL length to prevent browser limit issues + const urlSearchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + urlSearchParams.set(key, String(value)); + } + } + + const maxUrlLength = 2000; // Conservative limit for browser compatibility + if (urlSearchParams.toString().length > maxUrlLength) { + errors.push( + `URL parameters too long (${urlSearchParams.toString().length} chars). Maximum allowed: ${maxUrlLength}`, + ); + } + + return { + isValid: errors.length === 0, + errors, + }; +} + +// Helper function to check if any options differ from defaults +export function hasNonDefaultValues( + inputUrl: string, + packOptions: Record, + defaultOptions: Record, +): boolean { + // Check if there's input URL + if (inputUrl && inputUrl.trim() !== '') { + return true; + } + + // Check if any pack option differs from default + for (const [key, value] of Object.entries(packOptions)) { + const defaultValue = defaultOptions[key]; + if (typeof value === 'string' && typeof defaultValue === 'string') { + if (value.trim() !== defaultValue.trim()) { + return true; + } + } else if (value !== defaultValue) { + return true; + } + } + + return false; +} + +/** + * Parses URL query parameters and returns pack options and repository URL. + * Supports backward compatibility with 'style' parameter as alias for 'format'. + * + * @returns Parsed options object with repository URL and pack options + */ +export function parseUrlParameters(): Partial { + if (typeof window === 'undefined') { + return {}; + } + + const urlParams = new URLSearchParams(window.location.search); + const params: Partial = {}; + + // Repository URL parameter + const repo = urlParams.get('repo'); + if (repo) { + params.repo = repo.trim(); + } + + // Format and Style parameters (with conflict handling) + const format = urlParams.get('format'); + const style = urlParams.get('style'); + + if ( + format && + (VALID_FORMATS as readonly string[]).includes(format) && + style && + (VALID_FORMATS as readonly string[]).includes(style) + ) { + // Both present: prefer format, warn about conflict + params.format = format as (typeof VALID_FORMATS)[number]; + console.warn( + `Both 'format' and 'style' URL parameters are present. Using 'format=${format}' and ignoring 'style=${style}'.`, + ); + } else if (format && (VALID_FORMATS as readonly string[]).includes(format)) { + params.format = format as (typeof VALID_FORMATS)[number]; + } else if (style && (VALID_FORMATS as readonly string[]).includes(style)) { + params.format = style as (typeof VALID_FORMATS)[number]; + } + + // Include patterns + const include = urlParams.get('include'); + if (include) { + params.includePatterns = include; + } + + // Ignore patterns + const ignore = urlParams.get('ignore'); + if (ignore) { + params.ignorePatterns = ignore; + } + + // Boolean parameters + for (const param of BOOLEAN_PARAMS) { + const value = urlParams.get(param); + if (value !== null) { + // Accept various truthy values: true, 1, yes, on + params[param] = ['true', '1', 'yes', 'on'].includes(value.toLowerCase()); + } + } + + return params; +} + +/** + * Updates URL query parameters with the provided options. + * Validates parameters and provides error handling for URL length limits. + * + * @param options - Pack options and repository URL to set in URL + * @returns Result object with success status and optional error message + */ +export function updateUrlParameters(options: Partial): { + success: boolean; + error?: string; +} { + if (typeof window === 'undefined') { + return { success: false, error: 'Window object not available (SSR environment)' }; + } + + try { + // Validate parameters before updating URL + const validation = validateUrlParameters(options); + if (!validation.isValid) { + // If validation fails due to URL length, return error instead of continuing + const hasLengthError = validation.errors.some((error) => error.includes('too long')); + if (hasLengthError) { + return { success: false, error: validation.errors.join('; ') }; + } + // For other validation errors, just warn and continue + console.warn('URL parameter validation failed:', validation.errors); + } + + const url = new URL(window.location.href); + const params = url.searchParams; + + // Clear existing repomix-related parameters + for (const key of URL_PARAM_KEYS) { + params.delete(key); + } + + // Add new parameters + for (const [key, value] of Object.entries(options)) { + if (value !== undefined && value !== null) { + const urlParamKey = getUrlParamKey(key); + if (typeof value === 'boolean') { + params.set(urlParamKey, value.toString()); + } else if (typeof value === 'string' && value.trim() !== '') { + params.set(urlParamKey, value.trim()); + } + } + } + + // Update URL without reloading the page + window.history.replaceState({}, '', url.toString()); + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred while updating URL'; + console.error('Failed to update URL parameters:', errorMessage); + return { success: false, error: errorMessage }; + } +}