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
72 changes: 43 additions & 29 deletions src/components/InstallStep/OnboardingSteps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '@/components/Background';
import { Button } from '@/components/ui/button';
import { LocaleEnum, switchLanguage } from '@/i18n';
import { recordOnboardingStepCompleted } from '@/lib/events/appEvents';
import { cn } from '@/lib/utils';
import { useAuthStore, type WorkspaceMainBackground } from '@/store/authStore';
import {
Expand Down Expand Up @@ -123,16 +124,16 @@ function StepLanguage({
}) {
const { t } = useTranslation();
return (
<div className="gap-8 flex flex-col">
<div className="gap-2 flex flex-col items-center">
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center gap-2">
<span className="text-heading-base font-bold text-ds-text-neutral-default-default">
{t('layout.onboarding-setup-language-title')}
</span>
<span className="mt-2 text-body-base text-ds-text-neutral-muted-default">
<span className="text-body-base mt-2 text-ds-text-neutral-muted-default">
{t('layout.onboarding-setup-language-subtitle')}
</span>
</div>
<div className="gap-2 grid grid-cols-3">
<div className="grid grid-cols-3 gap-2">
{LANGUAGE_OPTIONS.map(({ key, nativeLabel }) => {
const active = selected === key;
const displayLabel =
Expand All @@ -142,10 +143,10 @@ function StepLanguage({
key={key}
onClick={() => onSelect(key)}
className={cn(
'rounded-xl px-6 py-3 text-body-sm font-medium transition-color flex items-center justify-between border border-solid duration-100',
'transition-color flex items-center justify-between rounded-xl border border-solid px-6 py-3 text-body-sm font-medium duration-100',
active
? 'border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default text-ds-text-neutral-default-default'
: 'bg-ds-bg-neutral-default-default text-ds-text-neutral-muted-default hover:bg-ds-bg-neutral-default-hover hover:border-ds-border-neutral-default-hover hover:text-ds-text-neutral-muted-hover border-transparent'
: 'border-transparent bg-ds-bg-neutral-default-default text-ds-text-neutral-muted-default hover:border-ds-border-neutral-default-hover hover:bg-ds-bg-neutral-default-hover hover:text-ds-text-neutral-muted-hover'
)}
>
<span>{displayLabel}</span>
Expand Down Expand Up @@ -194,27 +195,27 @@ function StepTheme({
];

return (
<div className="gap-8 flex flex-col">
<div className="flex flex-col gap-8">
{/* Mode selection */}
<div className="gap-4 flex w-full flex-col items-center">
<div className="flex w-full flex-col items-center gap-4">
<span className="text-heading-base font-bold text-ds-text-neutral-default-default">
{t('layout.onboarding-setup-appearance-title')}
</span>
<span className="mt-2 text-body-base text-ds-text-neutral-muted-default">
<span className="text-body-base mt-2 text-ds-text-neutral-muted-default">
{t('layout.onboarding-setup-language-subtitle')}
</span>
<div className="gap-3 flex w-full">
<div className="flex w-full gap-3">
{MODES.map(({ id, label, Icon }) => {
const active = appearanceMode === id;
return (
<button
key={id}
onClick={() => onModeChange(id)}
className={cn(
'gap-2 rounded-xl py-4 flex flex-1 flex-col items-center border border-solid transition-colors duration-100',
'flex flex-1 flex-col items-center gap-2 rounded-xl border border-solid py-4 transition-colors duration-100',
active
? 'border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default text-ds-text-neutral-default-default'
: 'bg-ds-bg-neutral-default-default text-ds-text-neutral-muted-default hover:bg-ds-bg-neutral-default-hover hover:border-ds-border-neutral-default-hover hover:text-ds-text-neutral-muted-hover border-transparent'
: 'border-transparent bg-ds-bg-neutral-default-default text-ds-text-neutral-muted-default hover:border-ds-border-neutral-default-hover hover:bg-ds-bg-neutral-default-hover hover:text-ds-text-neutral-muted-hover'
)}
>
<Icon size={20} strokeWidth={1.5} />
Expand All @@ -226,13 +227,13 @@ function StepTheme({
</div>

{/* Color theme selection */}
<div className="gap-4 flex w-full flex-col items-center">
<div className="flex w-full flex-col items-center gap-4">
<div>
<span className="text-body-lg font-semibold text-ds-text-neutral-default-default">
{t('layout.onboarding-setup-color-theme')}
</span>
</div>
<div className="gap-3 grid w-full grid-cols-4">
<div className="grid w-full grid-cols-4 gap-3">
{THEME_PRESETS.map(({ id, label, lightAccent, darkAccent }) => {
const accent = appearance === 'dark' ? darkAccent : lightAccent;
const active = activeThemeId === id;
Expand All @@ -241,10 +242,10 @@ function StepTheme({
key={id}
onClick={() => onThemeChange(id)}
className={cn(
'gap-3 rounded-xl p-4 flex flex-col items-center border border-solid transition-colors duration-100',
'flex flex-col items-center gap-3 rounded-xl border border-solid p-4 transition-colors duration-100',
active
? 'border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default'
: 'bg-ds-bg-neutral-default-default hover:bg-ds-bg-neutral-default-hover hover:border-ds-border-neutral-default-hover border-transparent'
: 'border-transparent bg-ds-bg-neutral-default-default hover:border-ds-border-neutral-default-hover hover:bg-ds-bg-neutral-default-hover'
)}
>
<div
Expand Down Expand Up @@ -295,17 +296,17 @@ function PatternPreviewCard({
<button
onClick={onSelect}
className={cn(
'gap-2 rounded-xl p-2 ition-colors flex flex-col items-center border border-solid',
'ition-colors flex flex-col items-center gap-2 rounded-xl border border-solid p-2',
selected
? 'border-ds-border-neutral-default-default bg-ds-bg-neutral-default-default'
: 'bg-ds-bg-neutral-default-default hover:border-ds-border-neutral-default-default border-transparent'
: 'border-transparent bg-ds-bg-neutral-default-default hover:border-ds-border-neutral-default-default'
)}
>
<div className="h-24 rounded-xl bg-ds-bg-neutral-subtle-default relative isolate w-full overflow-hidden">
<div className="relative isolate h-24 w-full overflow-hidden rounded-xl bg-ds-bg-neutral-subtle-default">
{Component && <Component />}
{selected && (
<div className="inset-0 absolute flex items-center justify-center">
<div className="bg-ds-bg-neutral-strong-default p-1 h-6 w-6 shadow-sm rounded-full">
<div className="absolute inset-0 flex items-center justify-center">
<div className="h-6 w-6 rounded-full bg-ds-bg-neutral-strong-default p-1 shadow-sm">
<Check
size={12}
strokeWidth={2.5}
Expand Down Expand Up @@ -338,16 +339,16 @@ function StepBackground({
}) {
const { t } = useTranslation();
return (
<div className="gap-8 flex w-full flex-col">
<div className="gap-2 flex flex-col items-center">
<div className="flex w-full flex-col gap-8">
<div className="flex flex-col items-center gap-2">
<span className="text-heading-base font-bold text-ds-text-neutral-default-default">
{t('layout.onboarding-setup-workspace-title')}
</span>
<span className="mt-2 text-body-base text-ds-text-neutral-muted-default">
<span className="text-body-base mt-2 text-ds-text-neutral-muted-default">
{t('layout.onboarding-setup-workspace-subtitle')}
</span>
</div>
<div className="gap-3 grid w-full grid-cols-3">
<div className="grid w-full grid-cols-3 gap-3">
{BG_PATTERN_DEFS.map(({ id, labelKey, Component }) => (
<PatternPreviewCard
key={id}
Expand Down Expand Up @@ -410,19 +411,26 @@ export function OnboardingSteps({ onComplete }: { onComplete: () => void }) {
setColorThemeForMode('dark', themeId);
};

const stepName = (s: Step) =>
s === 1 ? 'language' : s === 2 ? 'theme' : 'background';

const handleComplete = () => {
recordOnboardingStepCompleted({
step_id: 3,
step_name: 'background',
});
setOnboardingCompleted(true);
setIsFirstLaunch(false);
onComplete();
};

return (
<div className="rounded-2xl bg-ds-bg-neutral-subtle-default relative flex h-full w-full flex-col overflow-hidden">
<div className="relative flex h-full w-full flex-col overflow-hidden rounded-2xl bg-ds-bg-neutral-subtle-default">
{LivePatternComponent && <LivePatternComponent />}

<div className="px-8 py-6 relative z-[1] flex h-full flex-col">
<div className="relative z-[1] flex h-full flex-col px-8 py-6">
{/* Step indicator dots */}
<div className="mb-8 gap-2 flex items-center justify-center">
<div className="mb-8 flex items-center justify-center gap-2">
{([1, 2, 3] as Step[]).map((s) => (
<div
key={s}
Expand Down Expand Up @@ -482,7 +490,13 @@ export function OnboardingSteps({ onComplete }: { onComplete: () => void }) {
textWeight="semibold"
buttonContent="text"
buttonRadius="lg"
onClick={() => setStep((s) => (s + 1) as Step)}
onClick={() => {
recordOnboardingStepCompleted({
step_id: step,
step_name: stepName(step),
});
setStep((s) => (s + 1) as Step);
}}
>
{t('layout.continue')}
<ArrowRightIcon />
Expand Down
80 changes: 80 additions & 0 deletions src/lib/events/appEventClassifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========

/**
* Bucket raw runtime errors into a small, stable taxonomy. The raw message may
* include local paths, command output, or user content, so callers should emit
* only this low-cardinality label.
*/
export function classifyError(message?: string | null): string {
if (!message) return 'unknown';
const m = message.toLowerCase();
if (m.includes('backend') && (m.includes('ready') || m.includes('not')))
return 'backend_unavailable';
if (m.includes('already processing')) return 'single_agent_busy';
if (m.includes('credit') || m.includes('usage limit') || m.includes('quota'))
return 'credits_or_limit';
if (m.includes('model')) return 'model';
if (m.includes('mcp') || m.includes('tool') || m.includes('toolkit'))
return 'tool_or_mcp';
if (m.includes('network') || m.includes('timeout') || m.includes('fetch'))
return 'network';
return 'unknown';
}

/**
* Classify "what job is Eigent hired for" entirely on-device. The raw project
* name / summary is read locally but never emitted; only the resulting enum can
* be forwarded to edition-specific analytics adapters.
*/
const TASK_CATEGORY_RULES: Array<[string, RegExp]> = [
[
'coding',
/\b(code|coding|bug|debug|refactor|api|repo|git|deploy|compile|script|function|python|javascript|typescript)\b/,
],
[
'data_analysis',
/\b(data|csv|excel|spreadsheet|dataset|sql|chart|graph|statistic|analy)/,
],
[
'web_automation',
/\b(browse|browser|website|web page|navigate|crawl|scrape|fill.*form|automate)/,
],
[
'document',
/\b(pdf|document|\bdoc\b|slide|presentation|ppt|word file|convert.*file)/,
],
['communication', /\b(email|slack|message|notify|calendar|meeting|invite)\b/],
[
'design',
/\b(design|logo|figma|mockup|diagram|wireframe|image|illustration)\b/,
],
[
'writing',
/\b(write|blog|article|essay|draft|post|copywrit|content|translate|summari[sz]e)/,
],
[
'research',
/\b(research|find|search|investigate|gather|compare|look up|report on)/,
],
];

export function classifyTaskCategory(text?: string | null): string {
if (!text) return 'unknown';
const t = text.toLowerCase();
for (const [category, pattern] of TASK_CATEGORY_RULES) {
if (pattern.test(t)) return category;
}
return 'other';
}
Loading
Loading