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
4 changes: 4 additions & 0 deletions website/client/components/Home/TryIt.vue
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@
:error-type="errorType"
:repository-url="inputRepositoryUrl"
:pack-options="packOptions"
:progress-stage="progressStage"
:progress-message="progressMessage"
@repack="handleRepack"
/>
</div>
Expand Down Expand Up @@ -136,6 +138,8 @@ const {
errorType,
result,
hasExecuted,
progressStage,
progressMessage,

// Computed
isSubmitValid,
Expand Down
35 changes: 35 additions & 0 deletions website/client/components/Home/TryItLoading.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { PackProgressStage } from '../api/client';

interface Props {
stage?: PackProgressStage | null;
message?: string | null;
}

const props = defineProps<Props>();

const stageMessages: Record<PackProgressStage, string> = {
'cache-check': 'Checking cache...',
cloning: 'Cloning repository...',
'repository-fetch': 'Fetching repository...',
extracting: 'Extracting files...',
processing: 'Processing files...',
};

const MAX_DETAIL_LENGTH = 60;

const detailMessage = computed(() => {
const text = props.message || (props.stage && stageMessages[props.stage]) || '...';
if (text.length <= MAX_DETAIL_LENGTH) return text;
return `${text.slice(0, MAX_DETAIL_LENGTH)}...`;
});
</script>

<template>
<div class="loading">
<div class="loading-header">
<div class="spinner"></div>
<p>Processing repository...</p>
</div>
<p class="loading-detail">{{ detailMessage }}</p>
<div class="sponsor-section">
<p class="sponsor-header">Special thanks to:</p>
<a href="https://go.warp.dev/repomix" target="_blank" rel="noopener noreferrer">
Expand Down Expand Up @@ -40,6 +69,12 @@
margin: 0;
}

.loading-detail {
margin: 4px 0 0;
font-size: 0.8em;
color: var(--vp-c-text-3);
}

.spinner {
flex-shrink: 0;
width: 20px;
Expand Down
6 changes: 4 additions & 2 deletions website/client/components/Home/TryItResult.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { computed, ref } from 'vue';
import type { PackOptions } from '../../composables/usePackOptions';
import type { TabType } from '../../types/ui';
import type { FileInfo, PackResult } from '../api/client';
import type { FileInfo, PackProgressStage, PackResult } from '../api/client';
import SupportMessage from './SupportMessage.vue';
import TryItFileSelection from './TryItFileSelection.vue';
import TryItLoading from './TryItLoading.vue';
Expand All @@ -16,6 +16,8 @@ interface Props {
errorType?: 'error' | 'warning';
repositoryUrl?: string;
packOptions?: PackOptions;
progressStage?: PackProgressStage | null;
progressMessage?: string | null;
}

interface Emits {
Expand Down Expand Up @@ -51,7 +53,7 @@ const handleRepack = (selectedFiles: FileInfo[]) => {
<template>
<div class="result-viewer">
<template v-if="loading && !result">
<TryItLoading />
<TryItLoading :stage="progressStage" :message="progressMessage" />
<SupportMessage />
</template>
<TryItResultErrorContent
Expand Down
79 changes: 73 additions & 6 deletions website/client/components/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export interface PackRequest {
url: string;
format: 'xml' | 'markdown' | 'plain';
options: PackOptions;
signal?: AbortSignal;
file?: File;
}

Expand Down Expand Up @@ -61,9 +60,35 @@ export class ApiError extends Error {
}
}

export type PackProgressStage = 'cache-check' | 'cloning' | 'repository-fetch' | 'extracting' | 'processing';

export interface PackStreamCallbacks {
onProgress?: (stage: PackProgressStage, message?: string) => void;
signal?: AbortSignal;
}

const API_BASE_URL = import.meta.env.PROD ? 'https://api.repomix.com' : 'http://localhost:8080';

export async function packRepository(request: PackRequest): Promise<PackResult> {
// NDJSON stream event types
interface ProgressEvent {
type: 'progress';
stage: PackProgressStage;
message?: string;
}

interface ResultEvent {
type: 'result';
data: PackResult;
}

interface StreamErrorEvent {
type: 'error';
message: string;
}

type StreamEvent = ProgressEvent | ResultEvent | StreamErrorEvent;

export async function packRepository(request: PackRequest, callbacks?: PackStreamCallbacks): Promise<PackResult> {
const formData = new FormData();

if (request.file) {
Expand All @@ -77,14 +102,56 @@ export async function packRepository(request: PackRequest): Promise<PackResult>
const response = await fetch(`${API_BASE_URL}/api/pack`, {
method: 'POST',
body: formData,
signal: request.signal,
signal: callbacks?.signal,
});

const data = await response.json();

// Handle non-streaming error responses (validation errors return JSON)
if (!response.ok) {
const data = await response.json();
throw new ApiError((data as ErrorResponse).error);
}

return data as PackResult;
// Handle NDJSON stream
if (!response.body) {
throw new ApiError('No response body received');
}

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let result: PackResult | null = null;

try {
while (true) {
const { value, done } = await reader.read();
if (done) break;

buffer += decoder.decode(value, { stream: true });

// Parse complete lines from buffer
const lines = buffer.split('\n');
buffer = lines.pop() || '';

for (const line of lines) {
if (!line.trim()) continue;

const event = JSON.parse(line) as StreamEvent;
if (event.type === 'progress') {
callbacks?.onProgress?.(event.stage, event.message);
} else if (event.type === 'result') {
result = event.data;
} else if (event.type === 'error') {
throw new ApiError(event.message);
}
}
Comment thread
yamadashy marked this conversation as resolved.
}
} finally {
reader.releaseLock();
}

if (!result) {
throw new ApiError('No result received from server');
}

return result;
}
11 changes: 7 additions & 4 deletions website/client/components/utils/requestHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { PackOptions, PackRequest, PackResult } from '../api/client';
import type { PackOptions, PackProgressStage, PackRequest, PackResult } from '../api/client';
import { packRepository } from '../api/client';
import { type AnalyticsActionType, analyticsUtils } from './analytics';

interface RequestHandlerOptions {
onSuccess?: (result: PackResult) => void;
onError?: (error: string) => void;
onAbort?: (message: string) => void;
onProgress?: (stage: PackProgressStage, message?: string) => void;
signal?: AbortSignal;
file?: File;
}
Expand All @@ -19,7 +20,7 @@ export async function handlePackRequest(
options: PackOptions,
handlerOptions: RequestHandlerOptions = {},
): Promise<void> {
const { onSuccess, onError, onAbort, signal, file } = handlerOptions;
const { onSuccess, onError, onAbort, onProgress, signal, file } = handlerOptions;
const processedUrl = url.trim();

// Track pack start
Expand All @@ -30,11 +31,13 @@ export async function handlePackRequest(
url: processedUrl,
format,
options,
signal,
file,
};

const response = await packRepository(request);
const response = await packRepository(request, {
onProgress,
signal,
});

// Track successful pack
if (response.metadata.summary) {
Expand Down
12 changes: 11 additions & 1 deletion website/client/composables/usePackRequest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { computed, onMounted, ref } from 'vue';
import type { FileInfo, PackResult } from '../components/api/client';
import type { FileInfo, PackProgressStage, PackResult } from '../components/api/client';
import { handlePackRequest } from '../components/utils/requestHandlers';
import { isValidRemoteValue } from '../components/utils/validation';
import { parseUrlParameters } from '../utils/urlParams';
Expand All @@ -24,6 +24,8 @@ export function usePackRequest() {
const errorType = ref<'error' | 'warning'>('error');
const result = ref<PackResult | null>(null);
const hasExecuted = ref(false);
const progressStage = ref<PackProgressStage | null>(null);
const progressMessage = ref<string | null>(null);

// Request controller for cancellation
let requestController: AbortController | null = null;
Expand Down Expand Up @@ -71,6 +73,8 @@ export function usePackRequest() {
errorType.value = 'error';
result.value = null;
hasExecuted.value = true;
progressStage.value = null;
progressMessage.value = null;
inputRepositoryUrl.value = inputUrl.value;

// Set up automatic timeout
Expand All @@ -93,6 +97,10 @@ export function usePackRequest() {
error.value = message;
errorType.value = 'warning';
},
onProgress: (stage, message) => {
progressStage.value = stage;
progressMessage.value = message ?? null;
},
signal: requestController.signal,
file: mode.value === 'file' || mode.value === 'folder' ? uploadedFile.value || undefined : undefined,
},
Expand Down Expand Up @@ -175,6 +183,8 @@ export function usePackRequest() {
errorType,
result,
hasExecuted,
progressStage,
progressMessage,

// Computed
isSubmitValid,
Expand Down
2 changes: 1 addition & 1 deletion website/server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading