Merge branch 'main' into feat/allow-user-change-base-branch
This commit is contained in:
commit
26b3c95c5e
36 changed files with 1763 additions and 782 deletions
|
|
@ -66,5 +66,5 @@ def is_codeflash_employee(user_id: str) -> bool:
|
|||
return user_id in CODEFLASH_EMPLOYEE_GITHUB_IDS
|
||||
|
||||
|
||||
def should_hack_for_demo(user_id: str, source_code: str) -> bool:
|
||||
return bool(is_codeflash_employee(user_id) and "def find_common_tags(articles" in source_code)
|
||||
def should_hack_for_demo(source_code: str) -> bool:
|
||||
return bool("def find_common_tags(articles" in source_code)
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ async def optimize(
|
|||
sentry_sdk.capture_exception(e)
|
||||
return e.status_code, OptimizeErrorResponseSchema(error=e.message)
|
||||
|
||||
if should_hack_for_demo(request.user, ctx.source_code):
|
||||
if should_hack_for_demo(ctx.source_code):
|
||||
return 200, await hack_for_demo(ctx)
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -345,7 +345,7 @@ async def testgen(
|
|||
sentry_sdk.capture_exception(e)
|
||||
return e.status_code, TestGenErrorResponseSchema(error=e.message)
|
||||
|
||||
if should_hack_for_demo(request.user, data.source_code_being_tested):
|
||||
if should_hack_for_demo(data.source_code_being_tested):
|
||||
demo_hack_response = await hack_for_demo(data, python_version)
|
||||
return 200, demo_hack_response
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { vscode } from "./utils/vscode";
|
|||
import ChatView from "./components/chatView";
|
||||
import Tabs from "./components/tabs";
|
||||
import OptimizationQueue from "./components/optimizationQueue";
|
||||
import DemoOptimization from "./components/demoOptimization";
|
||||
import SignInForm from "./components/signInForm";
|
||||
|
||||
function App() {
|
||||
|
|
@ -28,20 +29,32 @@ function App() {
|
|||
return <SignInForm />;
|
||||
}
|
||||
|
||||
// https://cdn.displate.com/artwork/270x380/2024-11-23/60ffe7e4-4185-4ac8-9081-5268ba425a85.jpg
|
||||
const firstTime = !store.triedCodeflashDemo;
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<Tabs />
|
||||
<div className="content">
|
||||
{store.activeTab === "optimization" && <ChatView />}
|
||||
{store.activeTab === "tasks" && (
|
||||
<OptimizationQueue queueTasks={store.queueTasks} />
|
||||
)}
|
||||
</div>
|
||||
{/* <OptimizeCurrentDiff /> */}
|
||||
|
||||
{/* {store.functionsInCurrentFile.length > 0 && <CurrentFileFunctions />} */}
|
||||
{firstTime ? (
|
||||
<>
|
||||
<div className="gridBackground" />
|
||||
<DemoOptimization />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Tabs />
|
||||
<div className="content">
|
||||
{store.activeTab === "optimization" && <ChatView />}
|
||||
{store.activeTab === "tasks" && (
|
||||
<OptimizationQueue queueTasks={store.queueTasks} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// <OptimizeCurrentDiff />
|
||||
// {store.functionsInCurrentFile.length > 0 && <CurrentFileFunctions />}
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -61,12 +61,14 @@ interface ChatInputProps {
|
|||
fileSuggestions?: FileInWorkspace[];
|
||||
functionSuggestions?: { file: string; functions: string[] };
|
||||
currentOptimizationTask: QueueTaskItem | undefined;
|
||||
forDemo?: boolean;
|
||||
}
|
||||
|
||||
const ChatInput: React.FC<ChatInputProps> = ({
|
||||
export const ChatInput: React.FC<ChatInputProps> = ({
|
||||
fileSuggestions = [],
|
||||
functionSuggestions = { file: "", functions: [] },
|
||||
currentOptimizationTask,
|
||||
forDemo = false,
|
||||
}) => {
|
||||
const [outsideModuleRoot, setOutsideModuleRoot] = useState<boolean>(false);
|
||||
|
||||
|
|
@ -88,8 +90,12 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (forDemo) {
|
||||
return;
|
||||
}
|
||||
if (selectedFile) {
|
||||
checkOutsideModuleRoot(selectedFile);
|
||||
requestFunctionsFromFile(selectedFile);
|
||||
} else {
|
||||
setOutsideModuleRoot(false);
|
||||
}
|
||||
|
|
@ -242,7 +248,6 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
const handleSelect = (choice: string | FileInWorkspace) => {
|
||||
if (activeDropdown === "file") {
|
||||
const file = choice as FileInWorkspace;
|
||||
requestFunctionsFromFile(file.abs);
|
||||
setSelectedFile(file.abs);
|
||||
setSelectedFunction(undefined);
|
||||
inputRef.current?.focus(); // focus the input to select the function
|
||||
|
|
@ -379,7 +384,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
onKeyDown={handleKeyDown}
|
||||
readOnly={isRunning}
|
||||
/>
|
||||
{selectedFile && outsideModuleRoot && (
|
||||
{!forDemo && selectedFile && outsideModuleRoot && (
|
||||
<div className={styles.outsideModuleWarning}>
|
||||
<span className="codicon codicon-warning"></span>
|
||||
<span>
|
||||
|
|
@ -390,23 +395,29 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
<Tooltip
|
||||
position="left"
|
||||
content={isRunning ? "Stop" : "Optimize"}
|
||||
className={styles.tooltip}
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.button}
|
||||
disabled={!selectedFile || !selectedFunction}
|
||||
{forDemo ? (
|
||||
currentOptimizationTask?.status == "optimizing" && (
|
||||
<span className="codicon codicon-loading spin"></span>
|
||||
)
|
||||
) : (
|
||||
<Tooltip
|
||||
position="left"
|
||||
content={isRunning ? "Stop" : "Optimize"}
|
||||
className={styles.tooltip}
|
||||
>
|
||||
{isRunning ? (
|
||||
<span className="codicon codicon-debug-stop"></span>
|
||||
) : (
|
||||
<span className="codicon codicon-arrow-up"></span>
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.button}
|
||||
disabled={!selectedFile || !selectedFunction}
|
||||
>
|
||||
{isRunning ? (
|
||||
<span className="codicon codicon-debug-stop"></span>
|
||||
) : (
|
||||
<span className="codicon codicon-arrow-up"></span>
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@
|
|||
color: var(--vscode-editorWidget-foreground);
|
||||
border: 1px solid var(--vscode-editorWidget-border, transparent);
|
||||
border-radius: 7px;
|
||||
padding: 0.15rem 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.15rem 0.2rem;
|
||||
font-size: 0.85rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.chip :global(.codicon) {
|
||||
.chip :global(.codicon).close {
|
||||
padding: 0.1rem;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ const Chip = ({ children, onCloseClick, ...props }: Props) => {
|
|||
>
|
||||
{children}
|
||||
{onCloseClick && (
|
||||
<span className="codicon codicon-close" onClick={onCloseClick}></span>
|
||||
<span
|
||||
className={"codicon codicon-close " + styles.close}
|
||||
onClick={onCloseClick}
|
||||
></span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
import { useStore } from "../store/root";
|
||||
import { ChatInput } from "./chatView";
|
||||
import TaskLogging from "./tasksLogging";
|
||||
import styles from "../styles/demoWelcome.module.css";
|
||||
import type {
|
||||
MessageType,
|
||||
RequestStartDemoOptimizationMessage,
|
||||
} from "@codeflash/types";
|
||||
|
||||
declare const vscode: {
|
||||
postMessage: (message: any) => void;
|
||||
};
|
||||
|
||||
const DemoOptimization = () => {
|
||||
const demoTask = useStore((state) => state.demoOptTask);
|
||||
|
||||
const handleStartDemo = () => {
|
||||
const msg = {
|
||||
type: "requestStartDemoOptimization",
|
||||
} as RequestStartDemoOptimizationMessage;
|
||||
vscode.postMessage(msg);
|
||||
};
|
||||
|
||||
if (demoTask) {
|
||||
return (
|
||||
<>
|
||||
<ChatInput
|
||||
fileSuggestions={[]}
|
||||
functionSuggestions={{ file: "", functions: [] }}
|
||||
currentOptimizationTask={demoTask}
|
||||
forDemo
|
||||
/>
|
||||
<TaskLogging
|
||||
task={demoTask}
|
||||
onDemoSuccess={() => {
|
||||
vscode.postMessage({ type: "requestDemoOptimizationSuccess" });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Welcome screen for first-time users
|
||||
return (
|
||||
<div className={styles.welcomeContainer + " " + "gradiantBg"}>
|
||||
<div className={styles.logoContainer}>
|
||||
<div className="cf-logo">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
viewBox="0 0 48.1 32"
|
||||
>
|
||||
<path
|
||||
className="st0"
|
||||
d="M31.8.3h-9.4L5.6,16.9h9.4L0,31.7h10.5L31.3,10.3h-9.6L31.8.3Z"
|
||||
fill="black"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
<path className="st0" d="M34.6.3l-5.9,6.1h13.5L48,.3h-13.4Z" />
|
||||
<path className="st0" d="M34.3,10.3l-5.9,6h13.5l5.8-6.1h-13.4Z" />
|
||||
<path className="st0" d="M26.9,18.6l-5.9,6.1h13.5l5.8-6.1h-13.4Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className={styles.title}>Welcome to Codeflash</h1>
|
||||
<p className={styles.subtitle}>
|
||||
AI-powered Python optimization — faster code, verified results.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.featuresGrid}>
|
||||
<div className={styles.featureItem}>
|
||||
<i className={`codicon codicon-zap ${styles.featureIcon}`}></i>
|
||||
<span className={styles.featureText}>Optimize Python functions</span>
|
||||
</div>
|
||||
<div className={styles.featureItem}>
|
||||
<i className={`codicon codicon-graph ${styles.featureIcon}`}></i>
|
||||
<span className={styles.featureText}>Performance evaluation</span>
|
||||
</div>
|
||||
<div className={styles.featureItem}>
|
||||
<i className={`codicon codicon-lightbulb ${styles.featureIcon}`}></i>
|
||||
<span className={styles.featureText}>AI-powered suggestions</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.actionContainer}>
|
||||
<p className={styles.helpText}>
|
||||
Try Codeflash with a sample Python function to see how it works
|
||||
</p>
|
||||
<div className={styles.actionButtons}>
|
||||
<button
|
||||
className={styles.demoButton}
|
||||
onClick={handleStartDemo}
|
||||
title="Start a demo optimization to see Codeflash in action"
|
||||
>
|
||||
<i className={`codicon codicon-play ${styles.buttonIcon}`}></i>
|
||||
Try it out
|
||||
</button>
|
||||
<button
|
||||
className={styles.demoButton + " " + styles.skipButton}
|
||||
onClick={() =>
|
||||
vscode.postMessage({
|
||||
type: "requestSkipDemoOptimization" as MessageType,
|
||||
})
|
||||
}
|
||||
>
|
||||
Skip Demo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DemoOptimization;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import React from "react";
|
||||
import styles from "../styles/demoSuccess.module.css";
|
||||
|
||||
interface DemoSuccessProps {
|
||||
onGetStarted?: () => void;
|
||||
}
|
||||
|
||||
const DemoSuccess: React.FC<DemoSuccessProps> = ({ onGetStarted }) => {
|
||||
const handleGetStarted = () => {
|
||||
if (onGetStarted) {
|
||||
onGetStarted();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.demoSuccessContainer}>
|
||||
<div className={styles.successIcon}>
|
||||
<i className={`codicon codicon-check ${styles.successIconSymbol}`}></i>
|
||||
</div>
|
||||
|
||||
<h2 className={styles.title}>Demo Completed Successfully!</h2>
|
||||
<p className={styles.subtitle}>
|
||||
You've seen how Codeflash can optimize your Python code with AI. Ready
|
||||
to boost your own functions?
|
||||
</p>
|
||||
|
||||
<button
|
||||
className={styles.actionButton}
|
||||
onClick={handleGetStarted}
|
||||
title="Start optimizing your own Python code"
|
||||
>
|
||||
<i className={`codicon codicon-rocket ${styles.buttonIcon}`}></i>
|
||||
Get Started
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DemoSuccess;
|
||||
|
|
@ -3,132 +3,170 @@ import ReactMarkdown from "react-markdown";
|
|||
import styles from "./markdown.module.css";
|
||||
import CodeBlock from "../codeBlock/codeBlock";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import Chip from "../chip/chip";
|
||||
|
||||
interface MarkdownBlockProps {
|
||||
markdown?: string;
|
||||
currentOptimizedFunction?: string;
|
||||
}
|
||||
|
||||
const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
table: ({ children, ...props }: any) => {
|
||||
return (
|
||||
<div className="table-wrapper">
|
||||
<table {...props}>{children}</table>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
a: ({ href, children, ...props }: any) => {
|
||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
// Only process file:// protocol or local file paths
|
||||
const isLocalPath =
|
||||
href?.startsWith("file://") ||
|
||||
href?.startsWith("/") ||
|
||||
!href?.includes("://");
|
||||
const MarkdownBlock = memo(
|
||||
({ markdown, currentOptimizedFunction }: MarkdownBlockProps) => {
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
table: ({ children, ...props }: any) => {
|
||||
return (
|
||||
<div className="table-wrapper">
|
||||
<table {...props}>{children}</table>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
a: ({ href, children, ...props }: any) => {
|
||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
// Only process file:// protocol or local file paths
|
||||
const isLocalPath =
|
||||
href?.startsWith("file://") ||
|
||||
href?.startsWith("/") ||
|
||||
!href?.includes("://");
|
||||
|
||||
if (!isLocalPath) {
|
||||
return;
|
||||
if (!isLocalPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Handle absolute vs project-relative paths
|
||||
let filePath = href.replace("file://", "");
|
||||
|
||||
// Extract line number if present
|
||||
// const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
|
||||
// let values = undefined
|
||||
// if (match) {
|
||||
// filePath = match[1]
|
||||
// values = { line: parseInt(match[2]) }
|
||||
// }
|
||||
|
||||
// Add ./ prefix if needed
|
||||
if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
|
||||
filePath = "./" + filePath;
|
||||
}
|
||||
|
||||
// vscode.postMessage({
|
||||
// type: "openFile",
|
||||
// text: filePath,
|
||||
// values,
|
||||
// })
|
||||
};
|
||||
|
||||
return (
|
||||
<a {...props} href={href} onClick={handleClick}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
pre: ({ children }: any) => {
|
||||
// The structure from react-markdown v9 is: pre > code > text
|
||||
const codeEl = children as React.ReactElement;
|
||||
|
||||
if (!codeEl || !codeEl.props) {
|
||||
return <pre>{children}</pre>;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
const { className = "", children: codeChildren } = codeEl.props as {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
// Handle absolute vs project-relative paths
|
||||
let filePath = href.replace("file://", "");
|
||||
|
||||
// Extract line number if present
|
||||
// const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
|
||||
// let values = undefined
|
||||
// if (match) {
|
||||
// filePath = match[1]
|
||||
// values = { line: parseInt(match[2]) }
|
||||
// }
|
||||
|
||||
// Add ./ prefix if needed
|
||||
if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
|
||||
filePath = "./" + filePath;
|
||||
// Get the actual code text
|
||||
let codeString = "";
|
||||
if (typeof codeChildren === "string") {
|
||||
codeString = codeChildren;
|
||||
} else if (Array.isArray(codeChildren)) {
|
||||
codeString = codeChildren
|
||||
.filter((child) => typeof child === "string")
|
||||
.join("");
|
||||
}
|
||||
|
||||
// vscode.postMessage({
|
||||
// type: "openFile",
|
||||
// text: filePath,
|
||||
// values,
|
||||
// })
|
||||
};
|
||||
// Extract language from className
|
||||
const match = /language-(\w+)/.exec(className);
|
||||
const language = match ? match[1] : "text";
|
||||
// Wrap CodeBlock in a div to ensure proper separation
|
||||
return (
|
||||
<div style={{ margin: "1em 0" }}>
|
||||
<CodeBlock source={codeString} language={language!} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
code: ({ children, className, ...props }: any) => {
|
||||
const isSingleLineString =
|
||||
typeof children === "string" && children.split("\n").length === 1;
|
||||
|
||||
return (
|
||||
<a {...props} href={href} onClick={handleClick}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
pre: ({ children }: any) => {
|
||||
// The structure from react-markdown v9 is: pre > code > text
|
||||
const codeEl = children as React.ReactElement;
|
||||
const isFilePath = isSingleLineString && children.endsWith(".py");
|
||||
const isTargetFunction =
|
||||
isSingleLineString &&
|
||||
currentOptimizedFunction &&
|
||||
children === currentOptimizedFunction;
|
||||
|
||||
if (!codeEl || !codeEl.props) {
|
||||
return <pre>{children}</pre>;
|
||||
}
|
||||
if (isFilePath) {
|
||||
return (
|
||||
<Chip
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ background: "var(--code-block-bg" }}
|
||||
>
|
||||
<span
|
||||
className="codicon codicon-python"
|
||||
style={{ color: "var(--cyan)" }}
|
||||
></span>
|
||||
|
||||
const { className = "", children: codeChildren } = codeEl.props as {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
{children}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
if (isTargetFunction) {
|
||||
return (
|
||||
<Chip
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ background: "var(--code-block-bg" }}
|
||||
>
|
||||
<span className="codicon codicon-symbol-method"></span>
|
||||
{children}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// Get the actual code text
|
||||
let codeString = "";
|
||||
if (typeof codeChildren === "string") {
|
||||
codeString = codeChildren;
|
||||
} else if (Array.isArray(codeChildren)) {
|
||||
codeString = codeChildren
|
||||
.filter((child) => typeof child === "string")
|
||||
.join("");
|
||||
}
|
||||
|
||||
// Extract language from className
|
||||
const match = /language-(\w+)/.exec(className);
|
||||
const language = match ? match[1] : "text";
|
||||
// Wrap CodeBlock in a div to ensure proper separation
|
||||
return (
|
||||
<div style={{ margin: "1em 0" }}>
|
||||
<CodeBlock source={codeString} language={language!} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
code: ({ children, className, ...props }: any) => {
|
||||
// This handles inline code
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.markdown}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[
|
||||
remarkGfm,
|
||||
// () => {
|
||||
// return (tree: any) => {
|
||||
// visit(tree, "code", (node: any) => {
|
||||
// if (!node.lang) {
|
||||
// node.lang = "text"
|
||||
// } else if (node.lang.includes(".")) {
|
||||
// node.lang = node.lang.split(".").slice(-1)[0]
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
]}
|
||||
components={components}
|
||||
>
|
||||
{markdown || ""}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className={styles.markdown}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[
|
||||
remarkGfm,
|
||||
// () => {
|
||||
// return (tree: any) => {
|
||||
// visit(tree, "code", (node: any) => {
|
||||
// if (!node.lang) {
|
||||
// node.lang = "text"
|
||||
// } else if (node.lang.includes(".")) {
|
||||
// node.lang = node.lang.split(".").slice(-1)[0]
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
]}
|
||||
components={components}
|
||||
>
|
||||
{markdown || ""}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default MarkdownBlock;
|
||||
|
|
|
|||
|
|
@ -186,24 +186,15 @@ const SignInForm = () => {
|
|||
left: 0,
|
||||
right: 0,
|
||||
height: "100%",
|
||||
background: "linear-gradient(to bottom, #FFC043, transparent 60%)",
|
||||
opacity: 0.2,
|
||||
background: "var(--codeflash-transparent-gradient-bg)",
|
||||
opacity: 0.7,
|
||||
pointerEvents: "none",
|
||||
filter: "blur(60px)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Grid Background */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundImage:
|
||||
"linear-gradient(to right, var(--vscode-editorLineNumber-foreground) 1px, transparent 1px), linear-gradient(to bottom, var(--vscode-editorLineNumber-foreground) 1px, transparent 1px)",
|
||||
backgroundSize: "24px 24px",
|
||||
opacity: 0.03,
|
||||
}}
|
||||
/>
|
||||
<div className="gridBackground" />
|
||||
|
||||
{/* Main Container */}
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import styles from "./taskLogging.module.css";
|
|||
import MarkdownBlock from "./markdown/markdown";
|
||||
import CollapsibleFile from "./collapsible/collapsibleFile";
|
||||
import CodeBlock from "./codeBlock/codeBlock";
|
||||
import DemoSuccess from "./demoSuccess";
|
||||
import { memo, useEffect, useRef } from "react";
|
||||
import { vscode } from "../utils/vscode";
|
||||
import {
|
||||
|
|
@ -38,6 +39,7 @@ const LogItem = memo(
|
|||
log,
|
||||
isLast,
|
||||
isStillRunning,
|
||||
functionName,
|
||||
}: {
|
||||
log: LogEntry;
|
||||
isLast: boolean;
|
||||
|
|
@ -60,7 +62,10 @@ const LogItem = memo(
|
|||
<span className="codicon codicon-pass-filled" />
|
||||
)}
|
||||
<div className={contentClassName}>
|
||||
<MemoMarkdownBlock markdown={textLog.text} />
|
||||
<MemoMarkdownBlock
|
||||
markdown={textLog.text}
|
||||
currentOptimizedFunction={functionName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -87,7 +92,10 @@ const LogItem = memo(
|
|||
<span className="codicon codicon-pass-filled" />
|
||||
)}
|
||||
<div className={contentClassName}>
|
||||
<MemoMarkdownBlock markdown={markdownLog.markdown} />
|
||||
<MemoMarkdownBlock
|
||||
markdown={markdownLog.markdown}
|
||||
currentOptimizedFunction={functionName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -97,6 +105,7 @@ const LogItem = memo(
|
|||
<div className={contentClassName}>
|
||||
<MemoMarkdownBlock
|
||||
markdown={`| Not supported log type: ${log.type}`}
|
||||
currentOptimizedFunction={functionName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -105,7 +114,13 @@ const LogItem = memo(
|
|||
},
|
||||
);
|
||||
|
||||
const TaskLogging = ({ task }: { task: QueueTaskItem }) => {
|
||||
const TaskLogging = ({
|
||||
task,
|
||||
onDemoSuccess,
|
||||
}: {
|
||||
task: QueueTaskItem;
|
||||
onDemoSuccess?: () => void;
|
||||
}) => {
|
||||
const listRef = useRef<VirtuosoHandle>(null);
|
||||
const scrollerRef = useRef<HTMLElement>(null);
|
||||
|
||||
|
|
@ -125,6 +140,49 @@ const TaskLogging = ({ task }: { task: QueueTaskItem }) => {
|
|||
listRef.current?.scrollToIndex({ index: task.logs.length - 1 });
|
||||
}, []);
|
||||
|
||||
const getFooterOnSuccess = () => {
|
||||
return onDemoSuccess ? (
|
||||
// completed the demo optimization
|
||||
<div className={styles.logEntry}>
|
||||
<DemoSuccess onGetStarted={onDemoSuccess} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.logEntry}>
|
||||
<div className={styles.logContent}>
|
||||
<div
|
||||
style={{
|
||||
...footerContainerStyles,
|
||||
color: "var(--vscode-foreground)",
|
||||
background: "rgba(34, 134, 58, 0.15)",
|
||||
}}
|
||||
>
|
||||
<MemoMarkdownBlock
|
||||
markdown={`✅ Optimization completed successfully.`}
|
||||
/>
|
||||
<button
|
||||
className={styles.queueTaskPatch}
|
||||
onClick={() => {
|
||||
const msg: ViewPatchMessage = {
|
||||
type: "viewPatch",
|
||||
payload: {
|
||||
id: task.id,
|
||||
functionName: task.functionName,
|
||||
patchFile: task.patchFile!,
|
||||
explanation: task.explanation || "",
|
||||
speedupStr: task.speedupStr || "",
|
||||
},
|
||||
};
|
||||
vscode.postMessage(msg);
|
||||
}}
|
||||
>
|
||||
<span className="codicon codicon-eye"></span>
|
||||
<span>View Optimization</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className={styles.messagesContainer}>
|
||||
<Virtuoso
|
||||
|
|
@ -181,42 +239,7 @@ const TaskLogging = ({ task }: { task: QueueTaskItem }) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{task.status === "completed" && (
|
||||
<div className={styles.logEntry}>
|
||||
<div className={styles.logContent}>
|
||||
<div
|
||||
style={{
|
||||
...footerContainerStyles,
|
||||
color: "var(--vscode-foreground)",
|
||||
background: "rgba(34, 134, 58, 0.15)",
|
||||
}}
|
||||
>
|
||||
<MemoMarkdownBlock
|
||||
markdown={`✅ Optimization completed successfully.`}
|
||||
/>
|
||||
<button
|
||||
className={styles.queueTaskPatch}
|
||||
onClick={() => {
|
||||
const msg: ViewPatchMessage = {
|
||||
type: "viewPatch",
|
||||
payload: {
|
||||
id: task.id,
|
||||
functionName: task.functionName,
|
||||
patchFile: task.patchFile!,
|
||||
explanation: task.explanation || "",
|
||||
speedupStr: task.speedupStr || "",
|
||||
},
|
||||
};
|
||||
vscode.postMessage(msg);
|
||||
}}
|
||||
>
|
||||
<span className="codicon codicon-eye"></span>
|
||||
<span>View Optimization</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{task.status === "completed" && getFooterOnSuccess()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
:root {
|
||||
--codeflash-gradient-bg: linear-gradient(135deg, #ffc043, #685123);
|
||||
--step-gap: 4rem;
|
||||
}
|
||||
|
||||
|
|
@ -22,6 +21,8 @@ body {
|
|||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
|
|
@ -31,37 +32,6 @@ body {
|
|||
animation: fadeIn 0.6s ease;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 16px;
|
||||
padding: 0.6rem;
|
||||
background-image: var(--codeflash-gradient-bg);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
inset: -1px;
|
||||
background-image: var(--codeflash-gradient-bg);
|
||||
border-radius: 12px;
|
||||
opacity: 0.2;
|
||||
filter: blur(12px);
|
||||
z-index: -1;
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.logo .codicon {
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import type { LogEntry } from "@codeflash/types";
|
||||
import type { State } from "../root";
|
||||
|
||||
export const handleAddLogForDemo = (state: State, log: LogEntry): State => {
|
||||
const demoTask = state.demoOptTask;
|
||||
if (!demoTask) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
demoOptTask: {
|
||||
...demoTask,
|
||||
logs: [...demoTask.logs, log],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -15,11 +15,13 @@ export const handleRestoreStateFromCache = (
|
|||
running,
|
||||
focusedTaskId,
|
||||
queueTasks,
|
||||
triedCodeflashDemo,
|
||||
} = payload as {
|
||||
running?: boolean;
|
||||
queueTasks?: QueueTaskItem[];
|
||||
focusedTaskId: string;
|
||||
moduleRoot?: string;
|
||||
triedCodeflashDemo?: boolean;
|
||||
};
|
||||
// console.log(
|
||||
// `received restore state from cache message: ${message.type}, ${JSON.stringify(payload)}`,
|
||||
|
|
@ -27,6 +29,7 @@ export const handleRestoreStateFromCache = (
|
|||
return {
|
||||
...oldState,
|
||||
optimizationRunning: running ?? false,
|
||||
triedCodeflashDemo: triedCodeflashDemo ?? false,
|
||||
queueTasks: queueTasks ?? [],
|
||||
focusedTaskId,
|
||||
moduleRoot: moduleRoot ?? "",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import type { UpdateDemoOptimizationTaskMessage } from "@codeflash/types";
|
||||
import type { State } from "../root";
|
||||
|
||||
export const updateDemoTask = (
|
||||
oldState: State,
|
||||
payload: UpdateDemoOptimizationTaskMessage["payload"],
|
||||
): State => {
|
||||
return {
|
||||
...oldState,
|
||||
demoOptTask: oldState.demoOptTask
|
||||
? { ...oldState.demoOptTask, ...payload }
|
||||
: null,
|
||||
};
|
||||
};
|
||||
|
|
@ -13,12 +13,15 @@ import { handleUpdateState } from "./actions/updateSidebarState";
|
|||
import { handleUpdateQueueTasks } from "./actions/updateQueueTasks";
|
||||
import { handleRestoreStateFromCache } from "./actions/restoreStateFromMemory";
|
||||
import { handleAddLog } from "./actions/addLog";
|
||||
import { handleAddLogForDemo } from "./actions/addLogForDemo";
|
||||
import { updateDemoTask } from "./actions/updateDemoTask";
|
||||
|
||||
export type State = {
|
||||
moduleRoot: string;
|
||||
status: SidebarStatus;
|
||||
activeTab: SidebarTab;
|
||||
optimizationRunning: boolean;
|
||||
triedCodeflashDemo: boolean;
|
||||
queueTasks: QueueTaskItem[];
|
||||
functionsInCurrentFile: string[];
|
||||
focusedTaskId: string;
|
||||
|
|
@ -26,6 +29,7 @@ export type State = {
|
|||
filesInWorkspace: FileInWorkspace[];
|
||||
functionsInCurrentContextFile: { file: string; functions: string[] };
|
||||
isValidatingApiKey: boolean;
|
||||
demoOptTask: QueueTaskItem | null;
|
||||
};
|
||||
|
||||
export type Action = {
|
||||
|
|
@ -42,7 +46,11 @@ export type Action = {
|
|||
changeTaskFocus: (id: string) => void;
|
||||
setActiveTab: (tab: SidebarTab) => void;
|
||||
setApiKeyLoadingState: (loading: boolean) => void;
|
||||
initDemoOptTask: (task: QueueTaskItem) => void;
|
||||
addLog: (log: LogEntry) => void;
|
||||
addLogForDemo: (log: LogEntry) => void;
|
||||
clearDemoOptTask: () => void;
|
||||
updateDemoOptTask: (taskUpdate: Partial<QueueTaskItem>) => void;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
|
|
@ -57,6 +65,8 @@ const initialState: State = {
|
|||
filesInWorkspace: [],
|
||||
functionsInCurrentContextFile: { file: "", functions: [] },
|
||||
isValidatingApiKey: false,
|
||||
triedCodeflashDemo: false,
|
||||
demoOptTask: null,
|
||||
};
|
||||
|
||||
export const useStore = create<State & Action>((_set) => ({
|
||||
|
|
@ -85,4 +95,12 @@ export const useStore = create<State & Action>((_set) => ({
|
|||
addLog: (log: LogEntry) => _set((oldState) => handleAddLog(oldState, log)),
|
||||
setApiKeyLoadingState: (loading: boolean) =>
|
||||
_set((oldState) => ({ ...oldState, isValidatingApiKey: loading })),
|
||||
initDemoOptTask: (task: QueueTaskItem) =>
|
||||
_set((oldState) => ({ ...oldState, demoOptTask: task })),
|
||||
addLogForDemo: (log: LogEntry) =>
|
||||
_set((oldState) => handleAddLogForDemo(oldState, log)),
|
||||
updateDemoOptTask: (payload: Partial<QueueTaskItem>) =>
|
||||
_set((oldState) => updateDemoTask(oldState, payload)),
|
||||
clearDemoOptTask: () =>
|
||||
_set((oldState) => ({ ...oldState, demoOptTask: null })),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,235 @@
|
|||
.demoSuccessContainer {
|
||||
padding: 1.3rem;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--vscode-editor-background) 0%,
|
||||
var(--vscode-sideBar-background) 100%
|
||||
);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 12px;
|
||||
margin: 16px 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
animation: slideInSuccess 0.6s ease-out;
|
||||
}
|
||||
|
||||
.successIcon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--vscode-button-background);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 16px;
|
||||
animation: bounceIn 0.8s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
.successIconSymbol {
|
||||
font-size: 24px;
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
margin: 0 0 8px 0;
|
||||
animation: fadeInUp 0.6s ease-out 0.4s both;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin: 0 0 24px 0;
|
||||
line-height: 1.4;
|
||||
animation: fadeInUp 0.6s ease-out 0.6s both;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: var(--vscode-font-family);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
animation: fadeInUp 0.6s ease-out 0.8s both;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.actionButton::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.actionButton:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.actionButton:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.actionButton:active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.actionButton:focus {
|
||||
outline: 2px solid var(--vscode-focusBorder);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.buttonIcon {
|
||||
font-size: 16px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.actionButton:hover .buttonIcon {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
animation: fadeInUp 0.6s ease-out 1s both;
|
||||
}
|
||||
|
||||
.featureBadge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.featureBadgeIcon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes slideInSuccess {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 320px) {
|
||||
.demoSuccessContainer {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.features {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast support */
|
||||
@media (prefers-contrast: high) {
|
||||
.demoSuccessContainer {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
border: 2px solid var(--vscode-button-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.demoSuccessContainer,
|
||||
.successIcon,
|
||||
.title,
|
||||
.subtitle,
|
||||
.actionButton,
|
||||
.features {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.actionButton:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.actionButton::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
.welcomeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 24px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: var(--vscode-foreground);
|
||||
font-family: var(--vscode-font-family);
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.logoContainer {
|
||||
margin-bottom: 24px;
|
||||
opacity: 0;
|
||||
animation: slideUp 0.6s ease-out 0.2s forwards;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--vscode-button-background) 0%,
|
||||
var(--vscode-button-hoverBackground) 100%
|
||||
);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.logoIcon {
|
||||
font-size: 28px;
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--vscode-foreground);
|
||||
opacity: 0;
|
||||
animation: slideUp 0.6s ease-out 0.4s forwards;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin: 0 0 32px 0;
|
||||
line-height: 1.5;
|
||||
opacity: 0;
|
||||
animation: slideUp 0.6s ease-out 0.6s forwards;
|
||||
}
|
||||
|
||||
.featuresGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin-bottom: 32px;
|
||||
opacity: 0;
|
||||
animation: slideUp 0.6s ease-out 0.8s forwards;
|
||||
}
|
||||
|
||||
.featureItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.featureItem:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.featureIcon {
|
||||
font-size: 16px;
|
||||
color: #ffc043;
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.featureText {
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.actionContainer {
|
||||
opacity: 0;
|
||||
animation: slideUp 0.6s ease-out 1s forwards;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.demoButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: var(--vscode-font-family);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.skipButton {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
|
||||
.demoButton:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.skipButton:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
.demoButton:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.demoButton:focus {
|
||||
outline: 2px solid var(--vscode-focusBorder);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.skipButton:focus {
|
||||
outline: 2px solid var(--vscode-focusBorder);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.buttonIcon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-top: 16px;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.4s ease-out 1.2s forwards;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments for smaller panels */
|
||||
@media (max-width: 320px) {
|
||||
.welcomeContainer {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.logoIcon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.featuresGrid {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast theme support */
|
||||
@media (prefers-contrast: high) {
|
||||
.featureItem {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.demoButton {
|
||||
border: 2px solid var(--vscode-button-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.welcomeContainer,
|
||||
.logoContainer,
|
||||
.title,
|
||||
.subtitle,
|
||||
.featuresGrid,
|
||||
.actionContainer,
|
||||
.helpText {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.demoButton:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,17 @@
|
|||
black 5%
|
||||
);
|
||||
/* --code-block-bg: var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))cc; */
|
||||
--brand: #ffc043;
|
||||
--cyan: #306998;
|
||||
--codeflash-gradient-bg: linear-gradient(135deg, #ffc043, #685123);
|
||||
--codeflash-transparent-gradient-bg: linear-gradient(
|
||||
to bottom,
|
||||
#ffc0434f,
|
||||
transparent 60%
|
||||
);
|
||||
--duration: 5s;
|
||||
--scale: 0.6;
|
||||
--glow-size: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
@ -62,6 +73,34 @@ body {
|
|||
flex-direction: column;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
padding: 1rem 0;
|
||||
z-index: 2;
|
||||
}
|
||||
.gradiantBg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: var(--codeflash-transparent-gradient-bg);
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
.gridBackground {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to right,
|
||||
var(--vscode-editorLineNumber-foreground) 1px,
|
||||
transparent 1px
|
||||
),
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
var(--vscode-editorLineNumber-foreground) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 24px 24px;
|
||||
opacity: 0.03;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
|
@ -119,13 +158,6 @@ body {
|
|||
}
|
||||
/* end of api key container */
|
||||
|
||||
/* glowing animation */
|
||||
:root {
|
||||
--duration: 5s;
|
||||
--scale: 0.6;
|
||||
--glow-size: 100%;
|
||||
}
|
||||
|
||||
/* core glow element (empty element positioned over the container) */
|
||||
.glowEffect {
|
||||
pointer-events: none;
|
||||
|
|
@ -239,3 +271,34 @@ body {
|
|||
.table-wrapper tr:hover td {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.cf-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 16px;
|
||||
padding: 0.6rem;
|
||||
background-image: var(--codeflash-gradient-bg);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cf-logo::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
inset: -1px;
|
||||
background-image: var(--codeflash-gradient-bg);
|
||||
border-radius: 12px;
|
||||
opacity: 0.2;
|
||||
filter: blur(12px);
|
||||
z-index: -1;
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.cf-logo .codicon {
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
.container {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
padding: 16px;
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
font-family: var(--vscode-font-family);
|
||||
color: var(--vscode-foreground);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.container::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type {
|
||||
AppendLogToDemoOptimizationTaskMessage,
|
||||
ChangeTaskFocusFromBackendMessage,
|
||||
NewLogEntryMessage,
|
||||
RecievedFilesInWorkspace,
|
||||
|
|
@ -6,6 +7,8 @@ import type {
|
|||
RestoreStateMessage,
|
||||
SetActiveSidebarTabMessage,
|
||||
SetApiKeyLoadingStateMessage,
|
||||
StartDemoOptimizationMessage,
|
||||
UpdateDemoOptimizationTaskMessage,
|
||||
UpdateQueueTasksMessage,
|
||||
UpdateStateMessage,
|
||||
WebviewMessage,
|
||||
|
|
@ -60,5 +63,25 @@ export const messageHandler = (event: MessageEvent, store: State & Action) => {
|
|||
.payload;
|
||||
store.setApiKeyLoadingState(loading);
|
||||
break;
|
||||
case "startDemoOptimization":
|
||||
const { task } = (webviewMessage as StartDemoOptimizationMessage).payload;
|
||||
store.initDemoOptTask(task);
|
||||
break;
|
||||
case "updateDemoOptimizationTask":
|
||||
const payload = (webviewMessage as UpdateDemoOptimizationTaskMessage)
|
||||
.payload;
|
||||
store.updateDemoOptTask(payload);
|
||||
break;
|
||||
case "appendLogToDemoOptimizationTask":
|
||||
const { log: demoLog } = (
|
||||
webviewMessage as AppendLogToDemoOptimizationTaskMessage
|
||||
).payload;
|
||||
store.addLogForDemo(demoLog);
|
||||
break;
|
||||
case "clearDemoOptimizationTask":
|
||||
store.clearDemoOptTask();
|
||||
break;
|
||||
default:
|
||||
console.warn("Unhandled message type:", webviewMessage.type);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ export type MessageType =
|
|||
| "setActiveSidebarTab"
|
||||
| "setApiKeyLoadingState"
|
||||
| "cancelTask"
|
||||
| "submitInitForm"
|
||||
| "appendLogToDemoOptimizationTask"
|
||||
| "startDemoOptimization"
|
||||
| "clearDemoOptimizationTask"
|
||||
| "requestStartDemoOptimization"
|
||||
| "updateDemoOptimizationTask"
|
||||
| "requestSkipDemoOptimization"
|
||||
| "requestDemoOptimizationSuccess"
|
||||
| "signIn"
|
||||
| "authStarted"
|
||||
| "authCompleted"
|
||||
|
|
@ -130,6 +138,27 @@ export interface UpdateQueueTasksMessage extends WebviewMessage {
|
|||
};
|
||||
}
|
||||
|
||||
export interface AppendLogToDemoOptimizationTaskMessage extends WebviewMessage {
|
||||
type: "appendLogToDemoOptimizationTask";
|
||||
payload: {
|
||||
log: LogEntry;
|
||||
};
|
||||
}
|
||||
export interface UpdateDemoOptimizationTaskMessage extends WebviewMessage {
|
||||
type: "updateDemoOptimizationTask";
|
||||
payload: Partial<QueueTaskItem>;
|
||||
}
|
||||
|
||||
export interface ClearDemoOptimizationTaskMessage extends WebviewMessage {
|
||||
type: "clearDemoOptimizationTask";
|
||||
}
|
||||
export interface StartDemoOptimizationMessage extends WebviewMessage {
|
||||
type: "startDemoOptimization";
|
||||
payload: {
|
||||
task: QueueTaskItem;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SetApiKeyLoadingStateMessage extends WebviewMessage {
|
||||
type: "setApiKeyLoadingState";
|
||||
payload: {
|
||||
|
|
@ -180,6 +209,18 @@ export interface CreateSampleFileMessage extends WebviewMessage {
|
|||
type: "createSampleFile";
|
||||
}
|
||||
|
||||
export interface RequestStartDemoOptimizationMessage extends WebviewMessage {
|
||||
type: "requestStartDemoOptimization";
|
||||
}
|
||||
|
||||
export interface RequestSkipDemoOptimizationMessage extends WebviewMessage {
|
||||
type: "requestSkipDemoOptimization";
|
||||
}
|
||||
|
||||
export interface RequestDemoOptimizationSuccessMessage extends WebviewMessage {
|
||||
type: "requestDemoOptimizationSuccess";
|
||||
}
|
||||
|
||||
export interface ChangeTaskFocusMessage extends WebviewMessage {
|
||||
type: "changeTaskFocus";
|
||||
payload: {
|
||||
|
|
@ -352,6 +393,9 @@ export type IncomingWebviewMessage =
|
|||
| SignInMessage
|
||||
| ChangeTaskFocusMessage
|
||||
| SubmitInitFormMessage
|
||||
| RequestStartDemoOptimizationMessage
|
||||
| RequestSkipDemoOptimizationMessage
|
||||
| RequestDemoOptimizationSuccessMessage
|
||||
| CancelAuthMessage;
|
||||
|
||||
export type OutgoingWebviewMessage =
|
||||
|
|
@ -365,6 +409,11 @@ export type OutgoingWebviewMessage =
|
|||
| RecievedFunctionsMessage
|
||||
| ChangeTaskFocusFromBackendMessage
|
||||
| SetActiveSidebarTabMessage
|
||||
| SetApiKeyLoadingStateMessage
|
||||
| AppendLogToDemoOptimizationTaskMessage
|
||||
| StartDemoOptimizationMessage
|
||||
| UpdateDemoOptimizationTaskMessage
|
||||
| ClearDemoOptimizationTaskMessage
|
||||
| AuthStartedMessage
|
||||
| AuthCompletedMessage
|
||||
| AuthCancelledMessage
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const LSP_COMMANDS = {
|
|||
PERFORM_FUNCTION_OPTIMIZATION: "performFunctionOptimization",
|
||||
CLEANUP_CURRENT_OPTIMIZER_SESSION: "cleanupCurrentOptimizerSession",
|
||||
INIT_PROJECT: "initProject",
|
||||
START_DEMO_OPTIMIZATION: "startDemoOptimization",
|
||||
} as const;
|
||||
|
||||
export const PYTHON_EXTENSION_ID = "ms-python.python";
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { window, workspace } from "vscode";
|
|||
|
||||
export const enum GlobalStateKey {
|
||||
IsExtensionInstalled = "isExtensionInstalled",
|
||||
TriedCodeflashDemo = "triedCodeflashDemo",
|
||||
Running = "running",
|
||||
UserID = "user_id",
|
||||
QueueTasks = "queueTasks",
|
||||
|
|
@ -18,6 +19,7 @@ export type ContextFiles = Record<string, string>;
|
|||
interface GlobalStateKeyMapping {
|
||||
[GlobalStateKey.Running]: boolean;
|
||||
[GlobalStateKey.IsExtensionInstalled]: boolean;
|
||||
[GlobalStateKey.TriedCodeflashDemo]: boolean;
|
||||
[GlobalStateKey.UserID]: string | undefined;
|
||||
[GlobalStateKey.QueueTasks]: QueueTaskItem[];
|
||||
[GlobalStateKey.FocusedTaskId]: string | undefined;
|
||||
|
|
@ -89,6 +91,7 @@ export class GlobalState {
|
|||
focusedTaskId: this.get(GlobalStateKey.FocusedTaskId)!,
|
||||
queueTasks: this.get(GlobalStateKey.QueueTasks, [])!,
|
||||
moduleRoot: this.get(GlobalStateKey.ModuleRoot)!,
|
||||
triedCodeflashDemo: this.get(GlobalStateKey.TriedCodeflashDemo, false)!,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,11 @@ export class CodeflashCodeLensProvider
|
|||
return [];
|
||||
}
|
||||
|
||||
// don't provide codelens until user has tried the demo optimization once
|
||||
if (!this.globalState.get(GlobalStateKey.TriedCodeflashDemo, false)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.lspClient.state !== LanguageClientState.Running) {
|
||||
this.logger.warn(
|
||||
`CodeLens requested but LSP client not running. State: ${this.lspClient.state}`,
|
||||
|
|
|
|||
|
|
@ -206,7 +206,12 @@ export class GitPatchProvider {
|
|||
}
|
||||
|
||||
private removeFilesPrefixes = (path: string): string => {
|
||||
return path.replace("a/", "").replace("b/", "");
|
||||
if (path.startsWith("a/")) {
|
||||
return path.replace("a/", "");
|
||||
} else if (path.startsWith("b/")) {
|
||||
return path.replace("b/", "");
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
async applyPatch() {
|
||||
|
|
@ -215,10 +220,10 @@ export class GitPatchProvider {
|
|||
const patchContent = await fs.readFile(patchPath, { encoding: "utf-8" });
|
||||
applyPatches(patchContent, {
|
||||
loadFile: (index, callback) => {
|
||||
const filePath = this.removeFilesPrefixes(
|
||||
path.join(this.workspaceRoot, index.newFileName),
|
||||
const filePath = path.join(
|
||||
this.workspaceRoot,
|
||||
this.removeFilesPrefixes(index.newFileName),
|
||||
);
|
||||
|
||||
fs.readFile(filePath, { encoding: "utf-8" })
|
||||
.then((content) => callback(null, content))
|
||||
.catch((err) => callback(err, ""));
|
||||
|
|
|
|||
|
|
@ -248,55 +248,61 @@ export class InitWebviewProvider
|
|||
|
||||
private getHtmlContent(metaTags: string, scriptTag: string): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Codeflash</title>
|
||||
${metaTags}
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Codeflash</title>
|
||||
${metaTags}
|
||||
</head>
|
||||
<body>
|
||||
<div id="initview-container">
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
viewBox="0 0 48.1 32"
|
||||
>
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: black;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
class="st0"
|
||||
d="M31.8.3h-9.4L5.6,16.9h9.4L0,31.7h10.5L31.3,10.3h-9.6L31.8.3Z"
|
||||
/>
|
||||
<path class="st0" d="M34.6.3l-5.9,6.1h13.5L48,.3h-13.4Z" />
|
||||
<path class="st0" d="M34.3,10.3l-5.9,6h13.5l5.8-6.1h-13.4Z" />
|
||||
<path class="st0" d="M26.9,18.6l-5.9,6.1h13.5l5.8-6.1h-13.4Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1>Codeflash</h1>
|
||||
<div id="initview-container" class="gradiantBg">
|
||||
<div class="header">
|
||||
<div class="cf-logo">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
viewBox="0 0 48.1 32"
|
||||
>
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: black;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
class="st0"
|
||||
d="M31.8.3h-9.4L5.6,16.9h9.4L0,31.7h10.5L31.3,10.3h-9.6L31.8.3Z"
|
||||
/>
|
||||
<path class="st0" d="M34.6.3l-5.9,6.1h13.5L48,.3h-13.4Z" />
|
||||
<path class="st0" d="M34.3,10.3l-5.9,6h13.5l5.8-6.1h-13.4Z" />
|
||||
<path class="st0" d="M26.9,18.6l-5.9,6.1h13.5l5.8-6.1h-13.4Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1>Codeflash</h1>
|
||||
</div>
|
||||
|
||||
<div class="loading-wrapper" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p id="init-message">${this.currentInitMessage}</p>
|
||||
<div class="wave-wrapper">
|
||||
${[...Array(5)].map((_, i) => `<div class="wave-bar" style="animation-delay:${i * 100}ms" ></div>`).join("")}
|
||||
</div>
|
||||
<div class="spinner"></div>
|
||||
<p id="init-message">${this.currentInitMessage}</p>
|
||||
<div class="wave-wrapper">
|
||||
${[...Array(5)]
|
||||
.map(
|
||||
(_, i) =>
|
||||
`<div class="wave-bar" style="animation-delay:${i * 100}ms" ></div>`,
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
|
||||
<div class="timeline" id="steps-container"></div>
|
||||
</div>
|
||||
${scriptTag}
|
||||
|
||||
<div class="timeline" id="steps-container"></div>
|
||||
</div>
|
||||
${scriptTag}
|
||||
</body>
|
||||
</html>`;
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export class SidebarProvider
|
|||
private _currentFocusUri: vscode.Uri | null = null;
|
||||
private _currentFunctionCount = 0;
|
||||
private initializedOnce = false;
|
||||
private runningDemo = false;
|
||||
private openedPathches = new Map<string, GitPatchProvider>();
|
||||
|
||||
constructor(
|
||||
|
|
@ -115,7 +116,9 @@ export class SidebarProvider
|
|||
|
||||
private addLogEntryToUI(log: LogEntry) {
|
||||
this.sendMessage({
|
||||
type: "newLogEntry",
|
||||
type: this.runningDemo
|
||||
? "appendLogToDemoOptimizationTask"
|
||||
: "newLogEntry",
|
||||
payload: {
|
||||
log,
|
||||
},
|
||||
|
|
@ -211,7 +214,7 @@ export class SidebarProvider
|
|||
case "webviewReady":
|
||||
this._logger.debug("Webview reported ready. restoring previous state");
|
||||
this.retryUntilLspRunning(this.handleInitialState.bind(this));
|
||||
this.restorePrevStateRequest();
|
||||
this.syncUIState();
|
||||
break;
|
||||
case "navigateToFunction":
|
||||
await this.handleNavigateToFunction(
|
||||
|
|
@ -266,6 +269,15 @@ export class SidebarProvider
|
|||
vscode.window.showWarningMessage(message);
|
||||
}
|
||||
break;
|
||||
case "requestStartDemoOptimization":
|
||||
await this.handleStartDemoOptimization();
|
||||
break;
|
||||
case "requestSkipDemoOptimization":
|
||||
this.handleSkipDemoOptimization();
|
||||
break;
|
||||
case "requestDemoOptimizationSuccess":
|
||||
this.handleDemoOptimizationSuccess();
|
||||
break;
|
||||
case "signIn":
|
||||
await this.handleSignIn();
|
||||
break;
|
||||
|
|
@ -540,7 +552,67 @@ export class SidebarProvider
|
|||
}
|
||||
}
|
||||
|
||||
private restorePrevStateRequest(): void {
|
||||
private async handleStartDemoOptimization(): Promise<void> {
|
||||
try {
|
||||
this.runningDemo = true;
|
||||
const taskId = randomUUID().toString();
|
||||
const functionName = "find_common_tags";
|
||||
|
||||
const demoTask: QueueTaskItem = {
|
||||
id: taskId,
|
||||
status: "optimizing",
|
||||
functionName,
|
||||
filepath: `${functionName}.py`,
|
||||
logs: [],
|
||||
};
|
||||
|
||||
this.sendMessage({
|
||||
type: "startDemoOptimization",
|
||||
payload: {
|
||||
task: demoTask,
|
||||
},
|
||||
});
|
||||
|
||||
const initLog = this.getInitLog(taskId);
|
||||
this.sendMessage({
|
||||
type: "appendLogToDemoOptimizationTask",
|
||||
payload: {
|
||||
log: initLog,
|
||||
},
|
||||
});
|
||||
|
||||
const result =
|
||||
await this._optimizationService.runDemoOptimizationTask(functionName);
|
||||
if (result.status === "success") {
|
||||
this.sendMessage({
|
||||
type: "updateDemoOptimizationTask",
|
||||
payload: {
|
||||
status: "completed",
|
||||
},
|
||||
});
|
||||
this.globalState.set(GlobalStateKey.TriedCodeflashDemo, true);
|
||||
}
|
||||
} finally {
|
||||
this.runningDemo = false;
|
||||
}
|
||||
}
|
||||
|
||||
private handleDemoOptimizationSuccess(): void {
|
||||
this.globalState.set(GlobalStateKey.TriedCodeflashDemo, true);
|
||||
this.syncUIState();
|
||||
this.sendMessage({
|
||||
type: "clearDemoOptimizationTask",
|
||||
});
|
||||
this.refreshCodelens();
|
||||
}
|
||||
|
||||
private handleSkipDemoOptimization(): void {
|
||||
this.globalState.set(GlobalStateKey.TriedCodeflashDemo, true);
|
||||
this.syncUIState();
|
||||
this.refreshCodelens();
|
||||
}
|
||||
|
||||
private syncUIState(): void {
|
||||
const message: RestoreStateMessage = {
|
||||
type: "restoreStateFromCache",
|
||||
state: this.globalState.getStateForWebview(),
|
||||
|
|
@ -564,6 +636,15 @@ export class SidebarProvider
|
|||
await this.requestAnalysisFromServer(this._currentFocusUri);
|
||||
}
|
||||
|
||||
private getInitLog(taskId: string): LogEntry {
|
||||
return {
|
||||
id: randomUUID(),
|
||||
type: "text",
|
||||
text: "initializing codeflash optimization process",
|
||||
takes_time: true,
|
||||
task_id: taskId,
|
||||
};
|
||||
}
|
||||
private async handleNavigateToFunction(
|
||||
functionName: string,
|
||||
fileUri?: string,
|
||||
|
|
|
|||
|
|
@ -60,14 +60,22 @@ export class LspService {
|
|||
args,
|
||||
transport: TransportKind.stdio,
|
||||
options: {
|
||||
env: { ...process.env, CODEFLASH_LSP: "true" },
|
||||
env: {
|
||||
...process.env,
|
||||
CODEFLASH_LSP: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
debug: {
|
||||
transport: TransportKind.stdio,
|
||||
command,
|
||||
args: args,
|
||||
options: { env: { ...process.env, CODEFLASH_LSP: "true" } },
|
||||
options: {
|
||||
env: {
|
||||
...process.env,
|
||||
CODEFLASH_LSP: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -248,6 +248,20 @@ export class OptimizationService {
|
|||
return result;
|
||||
}
|
||||
|
||||
async runDemoOptimizationTask(
|
||||
functionName: string,
|
||||
): Promise<OptimizationResponse> {
|
||||
const result =
|
||||
await this.optimizationClient.sendRequest<OptimizationResponse>(
|
||||
LSP_COMMANDS.START_DEMO_OPTIMIZATION,
|
||||
{
|
||||
functionName: functionName,
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.logger.dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import LogoBox from "@/components/dashboard/logo-box"
|
|||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { generateToken } from "@/app/(dashboard)/apikeys/tokenfuncs"
|
||||
import { ApiKeyCard, StepInstruction } from "@/components/onboarding/step-content-renderer"
|
||||
import { ConfettiEffect } from "@/components/onboarding/confetti-effect"
|
||||
|
||||
// Type definitions to improve type safety
|
||||
interface ReferralOption {
|
||||
|
|
@ -87,8 +88,7 @@ const INSTALLATION_COMMANDS = {
|
|||
const ONBOARDING_STEPS = [
|
||||
{ id: 0, title: "How did you hear about us?" },
|
||||
{ id: 1, title: "Install & Initialize Codeflash" },
|
||||
{ id: 2, title: "Install GitHub app" },
|
||||
{ id: 3, title: "Setup Complete" },
|
||||
{ id: 2, title: "Setup Complete" },
|
||||
]
|
||||
|
||||
// Reusable animations
|
||||
|
|
@ -105,6 +105,7 @@ export default function OnboardingPage() {
|
|||
const [userId, setUserId] = useState<string>("")
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const [, setGithubAppInstalled] = useState(false)
|
||||
const [showConfetti, setShowConfetti] = useState(false)
|
||||
|
||||
// Onboarding step management
|
||||
const [stepsState, setStepsState] = useState<StepsState>({
|
||||
|
|
@ -147,6 +148,22 @@ export default function OnboardingPage() {
|
|||
}
|
||||
}, [showIntro])
|
||||
|
||||
// Show confetti when Setup Complete step is shown
|
||||
useEffect(() => {
|
||||
const currentStepIndex = getCurrentStep()
|
||||
if (currentStepIndex === 2) {
|
||||
// Show confetti when Setup Complete step is displayed
|
||||
setShowConfetti(true)
|
||||
// Hide confetti after 5 seconds
|
||||
const timer = setTimeout(() => {
|
||||
setShowConfetti(false)
|
||||
}, 5000)
|
||||
return () => clearTimeout(timer)
|
||||
} else {
|
||||
setShowConfetti(false)
|
||||
}
|
||||
}, [stepsState.current.cli])
|
||||
|
||||
// Check if user has completed onboarding and load referral source if available
|
||||
useEffect(() => {
|
||||
const checkOnboardingStatus = async () => {
|
||||
|
|
@ -256,7 +273,6 @@ export default function OnboardingPage() {
|
|||
if (userId) {
|
||||
await completeUserOnboarding(userId)
|
||||
}
|
||||
sessionStorage.setItem("showOnboardingConfetti", "true")
|
||||
router.push("/app/apikeys")
|
||||
} catch (error) {
|
||||
console.error("Failed to mark onboarding as completed:", error)
|
||||
|
|
@ -267,6 +283,9 @@ export default function OnboardingPage() {
|
|||
const handleGitHubAppInstall = () => {
|
||||
window.open("https://github.com/apps/codeflash-ai/installations/select_target", "_blank")
|
||||
setGithubAppInstalled(true)
|
||||
if (activeInitStep === 3) {
|
||||
setActiveInitStep(4)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle selecting a referral source and automatically submitting for non-other options
|
||||
|
|
@ -355,7 +374,7 @@ export default function OnboardingPage() {
|
|||
|
||||
// Reusable Loading component
|
||||
const LoadingSpinner = () => (
|
||||
<div className="flex justify-center items-center">
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -642,7 +661,7 @@ export default function OnboardingPage() {
|
|||
|
||||
<main className="flex-grow flex flex-col items-center justify-center max-w-3xl mx-auto -mt-12">
|
||||
<motion.div {...fadeInAnimation} className="w-full">
|
||||
<h1 className="text-2xl font-bold text-foreground mb-2">
|
||||
<h1 className="text-2xl font-bold text-foreground mb-2 mt-12">
|
||||
Install & Initialize Codeflash
|
||||
</h1>
|
||||
<p className="text-muted-foreground mb-8 font-medium">
|
||||
|
|
@ -660,7 +679,7 @@ export default function OnboardingPage() {
|
|||
{/* Installation Section */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">
|
||||
1. Install Codeflash
|
||||
Install Codeflash
|
||||
</h2>
|
||||
<Tabs defaultValue="pip" className="w-full">
|
||||
<TabsList className="w-full justify-start mb-4">
|
||||
|
|
@ -686,7 +705,7 @@ export default function OnboardingPage() {
|
|||
{/* Initialization Section */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">
|
||||
2. Initialize Codeflash
|
||||
Initialize Codeflash
|
||||
</h2>
|
||||
|
||||
{/* Step 1: Run Initialization */}
|
||||
|
|
@ -810,6 +829,47 @@ export default function OnboardingPage() {
|
|||
</div>
|
||||
)}
|
||||
</StepInstruction>
|
||||
|
||||
{/* Step 3: Install GitHub App */}
|
||||
<StepInstruction
|
||||
number={3}
|
||||
title="Install GitHub app"
|
||||
status={
|
||||
activeInitStep === 3
|
||||
? "active"
|
||||
: activeInitStep > 3
|
||||
? "completed"
|
||||
: "default"
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground font-medium">
|
||||
Codeflash requires this integration to open pull requests with the
|
||||
optimizations for your review.
|
||||
</p>
|
||||
<div className="flex justify-center">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={handleGitHubAppInstall}
|
||||
className="bg-[#24292f] hover:bg-[#0d1117] text-white transition-all duration-200
|
||||
rounded-lg flex items-center justify-center gap-2 px-6 py-3 shadow-lg font-medium"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="white"
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
<span className="font-medium">Install GitHub App</span>
|
||||
<ExternalLink className="w-4 h-4 ml-1" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</StepInstruction>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
@ -820,57 +880,10 @@ export default function OnboardingPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Connect with GitHub Step */}
|
||||
{/* Setup Complete Step */}
|
||||
{currentStepIndex === 2 && (
|
||||
<div className="min-h-screen flex flex-col p-6">
|
||||
<Header />
|
||||
|
||||
<main className="flex-grow flex flex-col items-center justify-center max-w-3xl mx-auto -mt-12">
|
||||
<motion.div {...fadeInAnimation} className="w-full">
|
||||
<h1 className="text-2xl font-bold text-foreground mb-2">Install GitHub app</h1>
|
||||
<p className="text-muted-foreground mb-8 font-medium">
|
||||
Codeflash requires this integration to open pull requests with the optimizations
|
||||
for your review.
|
||||
</p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="w-full p-6"
|
||||
>
|
||||
<div className="flex justify-center">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={handleGitHubAppInstall}
|
||||
className="bg-[#24292f] hover:bg-[#0d1117] text-white transition-all duration-200
|
||||
rounded-lg flex items-center justify-center gap-2 px-6 py-3 shadow-lg font-medium"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="white"
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
<span className="font-medium">Install GitHub App</span>
|
||||
<ExternalLink className="w-4 h-4 ml-1" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<NavigationButtons />
|
||||
</motion.div>
|
||||
</main>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Setup Complete Step */}
|
||||
{currentStepIndex === 3 && (
|
||||
<div className="min-h-screen flex flex-col p-6">
|
||||
{showConfetti && <ConfettiEffect />}
|
||||
<Header />
|
||||
|
||||
<main className="flex-grow flex flex-col items-center justify-center max-w-3xl mx-auto -mt-4">
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect, ReactNode } from "react"
|
||||
import { ConfettiEffect } from "@/components/onboarding/confetti-effect"
|
||||
|
||||
interface ApiKeysClientProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function ApiKeysClient({ children }: ApiKeysClientProps) {
|
||||
const [showConfetti, setShowConfetti] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Check for the session storage flag
|
||||
const shouldShowConfetti = sessionStorage.getItem("showOnboardingConfetti") === "true"
|
||||
|
||||
if (shouldShowConfetti) {
|
||||
// Remove the flag so it only happens once
|
||||
sessionStorage.removeItem("showOnboardingConfetti")
|
||||
|
||||
// Show the confetti
|
||||
setShowConfetti(true)
|
||||
|
||||
// Optionally hide confetti after a few seconds
|
||||
const timer = setTimeout(() => {
|
||||
setShowConfetti(false)
|
||||
}, 5000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{showConfetti && <ConfettiEffect />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import { Separator } from "@/components/ui/separator"
|
|||
import { ApiKeyTable } from "./api-key-table"
|
||||
import { type cf_api_keys, PrismaClient } from "@prisma/client"
|
||||
import PostHogClient from "@/lib/posthog"
|
||||
import { ApiKeysClient } from "./api-keys-client"
|
||||
import { VS_CODE_KEY_NAME } from "@codeflash-ai/common"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
|
@ -32,49 +31,47 @@ export default async function APIKeyGenerator(): Promise<JSX.Element> {
|
|||
await posthog.shutdown()
|
||||
|
||||
return (
|
||||
<ApiKeysClient>
|
||||
<div>
|
||||
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight max-w-full pb-1">
|
||||
API Keys
|
||||
</h3>
|
||||
<Separator />
|
||||
{apiKeys.length === 0 ? (
|
||||
<>
|
||||
<p className="leading-7 mt-6">
|
||||
Welcome! Check out the{" "}
|
||||
<a
|
||||
href="https://docs.codeflash.ai/getting-started/local-installation"
|
||||
target="_blank"
|
||||
className="underline"
|
||||
>
|
||||
Getting Started
|
||||
</a>{" "}
|
||||
docs, or create your first API key below to start using Codeflash.
|
||||
</p>
|
||||
<p>
|
||||
For help with setting up Codeflash on your codebase, please check out the Docs or{" "}
|
||||
<a
|
||||
href="https://calendly.com/codeflash-saurabh/codeflash-setup"
|
||||
target="_blank"
|
||||
className="underline"
|
||||
>
|
||||
book a call
|
||||
</a>{" "}
|
||||
with the founder.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
<p className="leading-7 mt-6">
|
||||
These API keys are used to authenticate your requests to Codeflash's AI services.
|
||||
</p>
|
||||
<ApiKeyTable apiKeys={apiKeys} vscodeKeyName={VS_CODE_KEY_NAME} />{" "}
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight max-w-full pb-1">
|
||||
API Keys
|
||||
</h3>
|
||||
<Separator />
|
||||
{apiKeys.length === 0 ? (
|
||||
<>
|
||||
<p className="leading-7 mt-6">
|
||||
Welcome! Check out the{" "}
|
||||
<a
|
||||
href="https://docs.codeflash.ai/getting-started/local-installation"
|
||||
target="_blank"
|
||||
className="underline"
|
||||
>
|
||||
Getting Started
|
||||
</a>{" "}
|
||||
docs, or create your first API key below to start using Codeflash.
|
||||
</p>
|
||||
<p>
|
||||
For help with setting up Codeflash on your codebase, please check out the Docs or{" "}
|
||||
<a
|
||||
href="https://calendly.com/codeflash-saurabh/codeflash-setup"
|
||||
target="_blank"
|
||||
className="underline"
|
||||
>
|
||||
book a call
|
||||
</a>{" "}
|
||||
with the founder.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
<p className="leading-7 mt-6">
|
||||
These API keys are used to authenticate your requests to Codeflash's AI services.
|
||||
</p>
|
||||
<ApiKeyTable apiKeys={apiKeys} vscodeKeyName={VS_CODE_KEY_NAME} />{" "}
|
||||
</>
|
||||
)}
|
||||
|
||||
<CreateApiKeyDialog />
|
||||
</div>
|
||||
</ApiKeysClient>
|
||||
<CreateApiKeyDialog />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,139 +45,132 @@ export async function getAllOptimizationEvents({
|
|||
}
|
||||
|
||||
if (filter) {
|
||||
// Merge filter into where, excluding review_quality since it's in a different table
|
||||
Object.keys(filter).forEach(key => {
|
||||
// COMMENTED OUT: Repository and quality filtering
|
||||
// if (key === "repository_id") {
|
||||
// where[key] = filter[key]
|
||||
// } else if (key !== "review_quality") {
|
||||
// where[key] = filter[key]
|
||||
// }
|
||||
|
||||
// Only apply non-repository and non-quality filters
|
||||
if (key !== "repository_id" && key !== "review_quality") {
|
||||
if (key === "repository_id") {
|
||||
where.AND = where.AND || []
|
||||
where.AND.push({ [key]: filter[key] })
|
||||
} else if (key !== "review_quality") {
|
||||
where[key] = filter[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// COMMENTED OUT: Check if we need to join with optimization_features for quality sorting/filtering
|
||||
// const needsOptimizationFeaturesJoin =
|
||||
// (sort && Object.keys(sort).some(k => k.toLowerCase() === "review_quality")) ||
|
||||
// (filter && Object.keys(filter).some(k => k.toLowerCase() === "review_quality"))
|
||||
|
||||
// Always use standard Prisma query (no quality joins)
|
||||
const needsOptimizationFeaturesJoin = false
|
||||
const needsOptimizationFeaturesJoin =
|
||||
(sort && Object.keys(sort).some(k => k.toLowerCase() === "review_quality")) ||
|
||||
(filter && Object.keys(filter).some(k => k.toLowerCase() === "review_quality"))
|
||||
|
||||
if (needsOptimizationFeaturesJoin) {
|
||||
// ENTIRE BLOCK COMMENTED OUT - This was for quality filtering/sorting
|
||||
// Build the WHERE clause for raw SQL
|
||||
// const whereConditions = []
|
||||
// const params: any[] = []
|
||||
// let paramIndex = 1
|
||||
// // Add is_staging condition
|
||||
// whereConditions.push(`oe.is_staging = true`)
|
||||
// // Add search conditions
|
||||
// if (search) {
|
||||
// whereConditions.push(
|
||||
// `(oe.function_name ILIKE $${paramIndex} OR oe.file_path ILIKE $${paramIndex})`,
|
||||
// )
|
||||
// params.push(`%${search}%`)
|
||||
// paramIndex += 1
|
||||
// }
|
||||
// // Add filter conditions
|
||||
// if (filter) {
|
||||
// if (filter.status) {
|
||||
// whereConditions.push(`oe.status = $${paramIndex}`)
|
||||
// params.push(filter.status)
|
||||
// paramIndex += 1
|
||||
// }
|
||||
// if (filter.event_type) {
|
||||
// whereConditions.push(`oe.event_type = $${paramIndex}`)
|
||||
// params.push(filter.event_type)
|
||||
// paramIndex += 1
|
||||
// }
|
||||
// if (filter.review_quality) {
|
||||
// whereConditions.push(`of.review_quality = $${paramIndex}`)
|
||||
// params.push(filter.review_quality)
|
||||
// paramIndex += 1
|
||||
// }
|
||||
// if (filter.repository_id !== undefined) {
|
||||
// if (filter.repository_id === null) {
|
||||
// whereConditions.push(`oe.repository_id IS NULL`)
|
||||
// } else if (filter.repository_id.not !== undefined && filter.repository_id.not === null) {
|
||||
// whereConditions.push(`oe.repository_id IS NOT NULL`)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// const whereClause = whereConditions.join(" AND ")
|
||||
// // Build ORDER BY clause - handle all sort fields
|
||||
// const orderByClauses: string[] = []
|
||||
// if (sort && Object.keys(sort).length > 0) {
|
||||
// Object.entries(sort).forEach(([key, direction]) => {
|
||||
// const dir = direction.toUpperCase()
|
||||
// if (key.toLowerCase() === "review_quality") {
|
||||
// // Special handling for review_quality (case-insensitive)
|
||||
// orderByClauses.push(`
|
||||
// CASE
|
||||
// WHEN LOWER(of.review_quality) = 'high' THEN 3
|
||||
// WHEN LOWER(of.review_quality) = 'medium' THEN 2
|
||||
// WHEN LOWER(of.review_quality) = 'low' THEN 1
|
||||
// ELSE 0
|
||||
// END ${dir}
|
||||
// `)
|
||||
// } else {
|
||||
// // Regular field sorting
|
||||
// orderByClauses.push(`oe.${key} ${dir}`)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// // Add default sort by created_at only if sort is falsy
|
||||
// if (!sort) {
|
||||
// orderByClauses.push("oe.created_at DESC")
|
||||
// }
|
||||
// const orderByClause = orderByClauses.join(", ")
|
||||
// // Get events with custom ordering - join with optimization_features table
|
||||
// const events = await prisma.$queryRawUnsafe<any[]>(
|
||||
// `
|
||||
// SELECT
|
||||
// oe.*,
|
||||
// of.review_quality,
|
||||
// of.review_explanation
|
||||
// FROM optimization_events oe
|
||||
// LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
|
||||
// WHERE ${whereClause}
|
||||
// ORDER BY ${orderByClause}
|
||||
// LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
// `,
|
||||
// ...params,
|
||||
// pageSize,
|
||||
// (page - 1) * pageSize,
|
||||
// )
|
||||
// // Get total count
|
||||
// const countResult = await prisma.$queryRawUnsafe<[{ count: bigint }]>(
|
||||
// `
|
||||
// SELECT COUNT(*) as count
|
||||
// FROM optimization_events oe
|
||||
// LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
|
||||
// WHERE ${whereClause}
|
||||
// `,
|
||||
// ...params,
|
||||
// )
|
||||
// const totalCount = Number(countResult[0].count)
|
||||
// // Fetch repository data for the events
|
||||
// const eventsWithRepo = await Promise.all(
|
||||
// events.map(async event => {
|
||||
// if (event.repository_id) {
|
||||
// const repository = await prisma.repositories.findUnique({
|
||||
// where: { id: event.repository_id },
|
||||
// })
|
||||
// return { ...event, repository }
|
||||
// }
|
||||
// return { ...event, repository: null }
|
||||
// }),
|
||||
// )
|
||||
// return { events: eventsWithRepo, totalCount }
|
||||
const whereConditions = []
|
||||
const params: any[] = []
|
||||
let paramIndex = 1
|
||||
whereConditions.push(`oe.is_staging = true`)
|
||||
if ("orgId" in payload) {
|
||||
whereConditions.push(`oe.repository_id IN (${repoIds.map(id => `'${id}'`).join(",")})`)
|
||||
} else {
|
||||
whereConditions.push(
|
||||
`(
|
||||
oe.repository_id IN (${repoIds.map(id => `'${id}'`).join(",")})
|
||||
OR oe.user_id = '${payload.userId}'
|
||||
OR oe.current_username = '${payload.username}'
|
||||
)`,
|
||||
)
|
||||
}
|
||||
// Add search conditions
|
||||
if (search) {
|
||||
whereConditions.push(
|
||||
`(oe.function_name ILIKE $${paramIndex} OR oe.file_path ILIKE $${paramIndex})`,
|
||||
)
|
||||
params.push(`%${search}%`)
|
||||
paramIndex += 1
|
||||
}
|
||||
// Add filter conditions
|
||||
if (filter) {
|
||||
if (filter.status) {
|
||||
whereConditions.push(`oe.status = $${paramIndex}`)
|
||||
params.push(filter.status)
|
||||
paramIndex += 1
|
||||
}
|
||||
if (filter.event_type) {
|
||||
whereConditions.push(`oe.event_type = $${paramIndex}`)
|
||||
params.push(filter.event_type)
|
||||
paramIndex += 1
|
||||
}
|
||||
if (filter.review_quality) {
|
||||
whereConditions.push(`of.review_quality = $${paramIndex}`)
|
||||
params.push(filter.review_quality)
|
||||
paramIndex += 1
|
||||
}
|
||||
if (filter.repository_id !== undefined) {
|
||||
if (filter.repository_id === null) {
|
||||
whereConditions.push(`oe.repository_id IS NULL`)
|
||||
} else if (filter.repository_id.not !== undefined && filter.repository_id.not === null) {
|
||||
whereConditions.push(`oe.repository_id IS NOT NULL`)
|
||||
}
|
||||
}
|
||||
}
|
||||
const whereClause = whereConditions.join(" AND ")
|
||||
const orderByClauses: string[] = []
|
||||
if (sort && Object.keys(sort).length > 0) {
|
||||
Object.entries(sort).forEach(([key, direction]) => {
|
||||
const dir = direction.toUpperCase()
|
||||
if (key.toLowerCase() === "review_quality") {
|
||||
orderByClauses.push(`
|
||||
CASE
|
||||
WHEN LOWER(of.review_quality) = 'high' THEN 3
|
||||
WHEN LOWER(of.review_quality) = 'medium' THEN 2
|
||||
WHEN LOWER(of.review_quality) = 'low' THEN 1
|
||||
ELSE 0
|
||||
END ${dir}
|
||||
`)
|
||||
} else {
|
||||
orderByClauses.push(`oe.${key} ${dir}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!sort) {
|
||||
orderByClauses.push("oe.created_at DESC")
|
||||
}
|
||||
const orderByClause = orderByClauses.join(", ")
|
||||
const events = await prisma.$queryRawUnsafe<any[]>(
|
||||
`
|
||||
SELECT
|
||||
oe.*,
|
||||
of.review_quality,
|
||||
of.review_explanation
|
||||
FROM optimization_events oe
|
||||
LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY ${orderByClause}
|
||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
`,
|
||||
...params,
|
||||
pageSize,
|
||||
(page - 1) * pageSize,
|
||||
)
|
||||
// Get total count
|
||||
const countResult = await prisma.$queryRawUnsafe<[{ count: bigint }]>(
|
||||
`
|
||||
SELECT COUNT(*) as count
|
||||
FROM optimization_events oe
|
||||
LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
|
||||
WHERE ${whereClause}
|
||||
`,
|
||||
...params,
|
||||
)
|
||||
const totalCount = Number(countResult[0].count)
|
||||
// Fetch repository data for the events
|
||||
const eventsWithRepo = await Promise.all(
|
||||
events.map(async event => {
|
||||
if (event.repository_id) {
|
||||
const repository = await prisma.repositories.findUnique({
|
||||
where: { id: event.repository_id },
|
||||
})
|
||||
return { ...event, repository }
|
||||
}
|
||||
return { ...event, repository: null }
|
||||
}),
|
||||
)
|
||||
return { events: eventsWithRepo, totalCount }
|
||||
} else {
|
||||
// Standard Prisma query with native orderBy
|
||||
const orderBy = sort || { created_at: "desc" }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"use client"
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { useState, useEffect, useCallback, useRef } from "react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
|
|
@ -30,13 +30,12 @@ import {
|
|||
ArrowDown,
|
||||
} from "lucide-react"
|
||||
import { formatDistanceToNow } from "date-fns"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { getUserId, getUserIdAndUsername } from "@/app/utils/auth"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { getAllOptimizationEvents } from "./action"
|
||||
import Image from "next/image"
|
||||
import { useViewMode } from "@/app/app/ViewModeContext"
|
||||
// COMMENTED OUT: Quality badge import
|
||||
import { ReviewQualityBadge } from "@/components/ui/quality_badge"
|
||||
|
||||
// Type definitions
|
||||
|
|
@ -70,6 +69,66 @@ interface OptimizationEvent {
|
|||
review_quality: string
|
||||
}
|
||||
|
||||
interface FilterState {
|
||||
search: string
|
||||
hasRepo: string
|
||||
status: string
|
||||
eventType: string
|
||||
reviewQuality: string
|
||||
sortBy: string
|
||||
page: number
|
||||
}
|
||||
|
||||
function TableSkeleton() {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="h-4 w-4 bg-muted animate-pulse rounded mt-1 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0 space-y-2">
|
||||
<div className="h-4 bg-muted animate-pulse rounded w-3/4" />
|
||||
<div className="h-3 bg-muted animate-pulse rounded w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 bg-muted animate-pulse rounded-full flex-shrink-0" />
|
||||
<div className="h-4 bg-muted animate-pulse rounded w-32" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="h-6 bg-muted animate-pulse rounded-full w-20 mx-auto" />
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="h-6 bg-muted animate-pulse rounded-full w-24 mx-auto" />
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="h-6 bg-muted animate-pulse rounded-full w-16 mx-auto" />
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="h-6 bg-muted animate-pulse rounded-full w-24 mx-auto" />
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="h-6 bg-muted animate-pulse rounded-full w-12" />
|
||||
<div className="h-6 bg-muted animate-pulse rounded-full w-12" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="h-3 w-3 bg-muted animate-pulse rounded flex-shrink-0" />
|
||||
<div className="h-4 bg-muted animate-pulse rounded w-24" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to calculate diff stats
|
||||
function calculateDiffStats(
|
||||
diffContents: Record<string, { oldContent: string; newContent: string }>,
|
||||
|
|
@ -126,22 +185,6 @@ function calculateDiffStats(
|
|||
}
|
||||
}
|
||||
|
||||
// Helper function to calculate coverage percentage
|
||||
function getCoverageInfo(diffContents: Record<string, { oldContent: string; newContent: string }>) {
|
||||
// This is a simplified coverage calculation
|
||||
// In practice, you'd want to integrate with actual coverage data
|
||||
const { totalAdditions, totalDeletions } = calculateDiffStats(diffContents)
|
||||
const totalLines = Object.values(diffContents).reduce((acc, { newContent }) => {
|
||||
return acc + newContent.split("\n").filter(line => line.trim() !== "").length
|
||||
}, 0)
|
||||
|
||||
// Simple heuristic: assume coverage decreases with more changes
|
||||
const changeRatio = (totalAdditions + totalDeletions) / Math.max(totalLines, 1)
|
||||
const coverage = Math.max(0.5, 1 - changeRatio * 0.3) // Keep between 50-100%
|
||||
|
||||
return Math.round(coverage * 100)
|
||||
}
|
||||
|
||||
// Client component for handling row clicks
|
||||
function ClickableTableRow({
|
||||
event,
|
||||
|
|
@ -152,13 +195,15 @@ function ClickableTableRow({
|
|||
children: React.ReactNode
|
||||
onRowClick: (eventId: string) => void
|
||||
}) {
|
||||
const handleRowClick = (e: React.MouseEvent) => {
|
||||
// Don't navigate if clicking on external link
|
||||
if ((e.target as HTMLElement).closest('a[href^="http"]')) {
|
||||
return
|
||||
}
|
||||
onRowClick(event.id)
|
||||
}
|
||||
const handleRowClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if ((e.target as HTMLElement).closest('a[href^="http"]')) {
|
||||
return
|
||||
}
|
||||
onRowClick(event.trace_id)
|
||||
},
|
||||
[event.trace_id, onRowClick],
|
||||
)
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
|
|
@ -175,37 +220,32 @@ export default function StagingPage() {
|
|||
const [userId, setUserId] = useState<string | null>(null)
|
||||
const [isLoadingUser, setIsLoadingUser] = useState(true)
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const [events, setEvents] = useState<OptimizationEvent[]>([])
|
||||
const [totalCount, setTotalCount] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Local page state instead of URL-based
|
||||
const [page, setPage] = useState(1)
|
||||
const search = searchParams.get("search") || ""
|
||||
// COMMENTED OUT: Repository filter from URL
|
||||
// const hasRepo = searchParams.get("hasRepo") || "all"
|
||||
const status = searchParams.get("status") || "all"
|
||||
const eventType = searchParams.get("eventType") || "all"
|
||||
// COMMENTED OUT: Quality filter from URL
|
||||
// const reviewQuality = searchParams.get("reviewQuality") || "all"
|
||||
const sortBy = searchParams.get("sortBy") || "created_at_desc"
|
||||
const pageSize = 10
|
||||
|
||||
// Local state for filter inputs
|
||||
const [searchInput, setSearchInput] = useState(search)
|
||||
// COMMENTED OUT: Repository filter state
|
||||
// const [hasRepoFilter, setHasRepoFilter] = useState(hasRepo)
|
||||
const [statusFilter, setStatusFilter] = useState(status)
|
||||
const [eventTypeFilter, setEventTypeFilter] = useState(eventType)
|
||||
// COMMENTED OUT: Quality filter state
|
||||
// const [reviewQualityFilter, setReviewQualityFilter] = useState(reviewQuality)
|
||||
const [sortByFilter, setSortByFilter] = useState(sortBy)
|
||||
const { currentOrg } = useViewMode()
|
||||
|
||||
// Load user ID on mount
|
||||
// Combined filter state
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
search: "",
|
||||
hasRepo: "all",
|
||||
status: "all",
|
||||
eventType: "all",
|
||||
reviewQuality: "all",
|
||||
sortBy: "created_at_desc",
|
||||
page: 1,
|
||||
})
|
||||
|
||||
const pageSize = 10
|
||||
|
||||
// Refs to track if initial load is done
|
||||
const isInitialMount = useRef(true)
|
||||
const debounceTimer = useRef<NodeJS.Timeout>()
|
||||
|
||||
// Load user ID on mount - only once
|
||||
useEffect(() => {
|
||||
const loadUserId = async () => {
|
||||
try {
|
||||
|
|
@ -221,6 +261,7 @@ export default function StagingPage() {
|
|||
loadUserId()
|
||||
}, [])
|
||||
|
||||
// Memoized load events function
|
||||
const loadEvents = useCallback(async () => {
|
||||
if (!userId) return
|
||||
|
||||
|
|
@ -230,28 +271,26 @@ export default function StagingPage() {
|
|||
try {
|
||||
const filter: Record<string, any> = {}
|
||||
|
||||
// COMMENTED OUT: Repository filter logic
|
||||
// if (hasRepo === "yes") {
|
||||
// filter.repository_id = { not: null }
|
||||
// } else if (hasRepo === "no") {
|
||||
// filter.repository_id = null
|
||||
// }
|
||||
|
||||
if (status !== "all") {
|
||||
filter.status = status
|
||||
if (filters.hasRepo === "yes") {
|
||||
filter.repository_id = { not: null }
|
||||
} else if (filters.hasRepo === "no") {
|
||||
filter.repository_id = null
|
||||
}
|
||||
|
||||
if (eventType !== "all") {
|
||||
filter.event_type = eventType
|
||||
if (filters.status !== "all") {
|
||||
filter.status = filters.status
|
||||
}
|
||||
|
||||
// COMMENTED OUT: Quality filter logic
|
||||
// if (reviewQuality !== "all") {
|
||||
// filter.review_quality = reviewQuality
|
||||
// }
|
||||
if (filters.eventType !== "all") {
|
||||
filter.event_type = filters.eventType
|
||||
}
|
||||
|
||||
if (filters.reviewQuality !== "all") {
|
||||
filter.review_quality = filters.reviewQuality
|
||||
}
|
||||
|
||||
// Parse sort parameter
|
||||
const [sortField, sortDirection] = sortBy.split("_").reduce(
|
||||
const [sortField, sortDirection] = filters.sortBy.split("_").reduce(
|
||||
(acc, part, index, arr) => {
|
||||
if (index === arr.length - 1 && (part === "asc" || part === "desc")) {
|
||||
return [acc[0], part]
|
||||
|
|
@ -264,15 +303,16 @@ export default function StagingPage() {
|
|||
const sort: Record<string, "asc" | "desc"> = {
|
||||
[sortField]: sortDirection as "asc" | "desc",
|
||||
}
|
||||
|
||||
const userSession = await getUserIdAndUsername()
|
||||
const data = await getAllOptimizationEvents({
|
||||
payload: currentOrg
|
||||
? { orgId: currentOrg.id }
|
||||
: { userId: userSession.userId, username: userSession.username },
|
||||
search,
|
||||
search: filters.search,
|
||||
filter,
|
||||
sort,
|
||||
page,
|
||||
page: filters.page,
|
||||
pageSize,
|
||||
})
|
||||
|
||||
|
|
@ -295,105 +335,116 @@ export default function StagingPage() {
|
|||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [
|
||||
userId,
|
||||
search,
|
||||
// COMMENTED OUT: hasRepo dependency
|
||||
// hasRepo,
|
||||
status,
|
||||
eventType,
|
||||
// COMMENTED OUT: reviewQuality dependency
|
||||
// reviewQuality,
|
||||
sortBy,
|
||||
page,
|
||||
pageSize,
|
||||
currentOrg,
|
||||
])
|
||||
}, [userId, filters, currentOrg, pageSize])
|
||||
|
||||
// Load events when filters change - with debounce for search
|
||||
useEffect(() => {
|
||||
if (!isLoadingUser && userId) {
|
||||
loadEvents()
|
||||
// Skip initial mount
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false
|
||||
loadEvents()
|
||||
return
|
||||
}
|
||||
|
||||
// Clear existing timer
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current)
|
||||
}
|
||||
|
||||
// Debounce only for search changes
|
||||
const hasSearchChanged = filters.search !== ""
|
||||
if (hasSearchChanged) {
|
||||
debounceTimer.current = setTimeout(() => {
|
||||
loadEvents()
|
||||
}, 300)
|
||||
} else {
|
||||
loadEvents()
|
||||
}
|
||||
}
|
||||
}, [userId, isLoadingUser, loadEvents])
|
||||
|
||||
const handleRowClick = (traceId: string) => {
|
||||
router.push(`/review-optimizations/${traceId}`)
|
||||
}
|
||||
return () => {
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current)
|
||||
}
|
||||
}
|
||||
}, [userId, isLoadingUser, filters, loadEvents])
|
||||
|
||||
// Update URL when filters change
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
const params = new URLSearchParams()
|
||||
if (searchInput) params.set("search", searchInput)
|
||||
// COMMENTED OUT: Repository filter param
|
||||
// if (hasRepoFilter !== "all") params.set("hasRepo", hasRepoFilter)
|
||||
if (statusFilter !== "all") params.set("status", statusFilter)
|
||||
if (eventTypeFilter !== "all") params.set("eventType", eventTypeFilter)
|
||||
// COMMENTED OUT: Quality filter param
|
||||
// if (reviewQualityFilter !== "all") params.set("reviewQuality", reviewQualityFilter)
|
||||
if (sortByFilter !== "created_at_desc") params.set("sortBy", sortByFilter)
|
||||
const handleRowClick = useCallback(
|
||||
(traceId: string) => {
|
||||
router.push(`/review-optimizations/${traceId}`)
|
||||
},
|
||||
[router],
|
||||
)
|
||||
|
||||
// Reset to page 1 when filters change
|
||||
setPage(1)
|
||||
router.push(`?${params.toString()}`)
|
||||
}, 300) // 300ms debounce
|
||||
// Update filter functions
|
||||
const updateFilter = useCallback((key: keyof FilterState, value: string | number) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
// Reset page when filters change (except page itself)
|
||||
...(key !== "page" && { page: 1 }),
|
||||
}))
|
||||
}, [])
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [
|
||||
searchInput,
|
||||
// COMMENTED OUT: hasRepoFilter dependency
|
||||
// hasRepoFilter,
|
||||
statusFilter,
|
||||
eventTypeFilter,
|
||||
// COMMENTED OUT: reviewQualityFilter dependency
|
||||
// reviewQualityFilter,
|
||||
sortByFilter,
|
||||
router,
|
||||
])
|
||||
|
||||
const clearFilters = () => {
|
||||
setSearchInput("")
|
||||
// COMMENTED OUT: Reset repository filter
|
||||
// setHasRepoFilter("all")
|
||||
setStatusFilter("all")
|
||||
setEventTypeFilter("all")
|
||||
// COMMENTED OUT: Reset quality filter
|
||||
// setReviewQualityFilter("all")
|
||||
setSortByFilter("created_at_desc")
|
||||
setPage(1)
|
||||
router.push("/review-optimizations")
|
||||
}
|
||||
const clearFilters = useCallback(() => {
|
||||
setFilters({
|
||||
search: "",
|
||||
hasRepo: "all",
|
||||
status: "all",
|
||||
eventType: "all",
|
||||
reviewQuality: "all",
|
||||
sortBy: "created_at_desc",
|
||||
page: 1,
|
||||
})
|
||||
}, [])
|
||||
|
||||
const hasActiveFilters =
|
||||
searchInput ||
|
||||
// COMMENTED OUT: hasRepoFilter check
|
||||
// hasRepoFilter !== "all" ||
|
||||
statusFilter !== "all" ||
|
||||
eventTypeFilter !== "all" ||
|
||||
// COMMENTED OUT: reviewQualityFilter check
|
||||
// reviewQualityFilter !== "all" ||
|
||||
sortByFilter !== "created_at_desc"
|
||||
filters.search ||
|
||||
filters.hasRepo !== "all" ||
|
||||
filters.status !== "all" ||
|
||||
filters.eventType !== "all" ||
|
||||
filters.reviewQuality !== "all" ||
|
||||
filters.sortBy !== "created_at_desc"
|
||||
|
||||
const totalPages = Math.ceil(totalCount / pageSize)
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
if (newPage >= 1 && newPage <= totalPages) {
|
||||
setPage(newPage)
|
||||
}
|
||||
}
|
||||
const handlePageChange = useCallback(
|
||||
(newPage: number) => {
|
||||
if (newPage >= 1 && newPage <= totalPages) {
|
||||
updateFilter("page", newPage)
|
||||
}
|
||||
},
|
||||
[totalPages, updateFilter],
|
||||
)
|
||||
|
||||
const getSortIcon = (field: string) => {
|
||||
if (sortByFilter.startsWith(field)) {
|
||||
return sortByFilter.endsWith("_asc") ? (
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
return <ArrowUpDown className="h-4 w-4 opacity-50" />
|
||||
}
|
||||
const getSortIcon = useCallback(
|
||||
(field: string) => {
|
||||
if (filters.sortBy.startsWith(field)) {
|
||||
return filters.sortBy.endsWith("_asc") ? (
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
return <ArrowUpDown className="h-4 w-4 opacity-50" />
|
||||
},
|
||||
[filters.sortBy],
|
||||
)
|
||||
|
||||
const getSpeedupBadge = (speedup?: number, speedupPct?: number) => {
|
||||
const toggleSort = useCallback(
|
||||
(field: string) => {
|
||||
const newSort = filters.sortBy.startsWith(field)
|
||||
? filters.sortBy === `${field}_desc`
|
||||
? `${field}_asc`
|
||||
: `${field}_desc`
|
||||
: `${field}_desc`
|
||||
updateFilter("sortBy", newSort)
|
||||
},
|
||||
[filters.sortBy, updateFilter],
|
||||
)
|
||||
|
||||
const getSpeedupBadge = useCallback((speedup?: number, speedupPct?: number) => {
|
||||
if (typeof speedup !== "number" || typeof speedupPct !== "number") return null
|
||||
|
||||
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
|
||||
|
|
@ -423,9 +474,9 @@ export default function StagingPage() {
|
|||
{speedup.toFixed(2)}x ({speedupPct.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")}%)
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getStatusBadge = (status?: string) => {
|
||||
const getStatusBadge = useCallback((status?: string) => {
|
||||
if (!status) return null
|
||||
|
||||
const variants: Record<string, { className: string; label: string }> = {
|
||||
|
|
@ -451,9 +502,9 @@ export default function StagingPage() {
|
|||
{variant.label}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getEventTypeBadge = (eventType?: string) => {
|
||||
const getEventTypeBadge = useCallback((eventType?: string) => {
|
||||
if (!eventType) return null
|
||||
|
||||
const variants: Record<string, { className: string; label: string }> = {
|
||||
|
|
@ -489,7 +540,7 @@ export default function StagingPage() {
|
|||
{variant.label}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (isLoadingUser) {
|
||||
return (
|
||||
|
|
@ -523,8 +574,8 @@ export default function StagingPage() {
|
|||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Search by function name or file path..."
|
||||
value={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
value={filters.search}
|
||||
onChange={e => updateFilter("search", e.target.value)}
|
||||
className="pl-10 w-full"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -536,19 +587,18 @@ export default function StagingPage() {
|
|||
</div>
|
||||
|
||||
{/* Filter Controls */}
|
||||
|
||||
{/* <Select value={hasRepoFilter} onValueChange={setHasRepoFilter}>
|
||||
<Select value={filters.hasRepo} onValueChange={value => updateFilter("hasRepo", value)}>
|
||||
<SelectTrigger className="w-[140px] sm:w-[180px]">
|
||||
<SelectValue placeholder="Repository" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Repositories</SelectItem>
|
||||
<SelectItem value="yes">Has Repository</SelectItem>
|
||||
<SelectItem value="no">No Repository</SelectItem>
|
||||
<SelectItem value="yes">With Repository</SelectItem>
|
||||
<SelectItem value="no">Without Repository</SelectItem>
|
||||
</SelectContent>
|
||||
</Select> */}
|
||||
</Select>
|
||||
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<Select value={filters.status} onValueChange={value => updateFilter("status", value)}>
|
||||
<SelectTrigger className="w-[120px] sm:w-[150px]">
|
||||
<SelectValue placeholder="Status" />
|
||||
</SelectTrigger>
|
||||
|
|
@ -559,7 +609,10 @@ export default function StagingPage() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={eventTypeFilter} onValueChange={setEventTypeFilter}>
|
||||
<Select
|
||||
value={filters.eventType}
|
||||
onValueChange={value => updateFilter("eventType", value)}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] sm:w-[150px]">
|
||||
<SelectValue placeholder="Event Type" />
|
||||
</SelectTrigger>
|
||||
|
|
@ -572,20 +625,22 @@ export default function StagingPage() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* COMMENTED OUT: Quality Filter */}
|
||||
{/* <Select value={reviewQualityFilter} onValueChange={setReviewQualityFilter}>
|
||||
<Select
|
||||
value={filters.reviewQuality}
|
||||
onValueChange={value => updateFilter("reviewQuality", value)}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] sm:w-[150px]">
|
||||
<SelectValue placeholder="Quality" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All qualities</SelectItem>
|
||||
<SelectItem value="all">All Quality</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
</SelectContent>
|
||||
</Select> */}
|
||||
</Select>
|
||||
|
||||
<Select value={sortByFilter} onValueChange={setSortByFilter}>
|
||||
<Select value={filters.sortBy} onValueChange={value => updateFilter("sortBy", value)}>
|
||||
<SelectTrigger className="w-[140px] sm:w-[200px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
|
|
@ -594,9 +649,8 @@ export default function StagingPage() {
|
|||
<SelectItem value="created_at_asc">Oldest</SelectItem>
|
||||
<SelectItem value="speedup_x_desc">Speedup (Highest)</SelectItem>
|
||||
<SelectItem value="speedup_x_asc">Speedup (Lowest)</SelectItem>
|
||||
{/* COMMENTED OUT: Quality sort options */}
|
||||
{/* <SelectItem value="review_quality_desc">Quality (High to Low)</SelectItem>
|
||||
<SelectItem value="review_quality_asc">Quality (Low to High)</SelectItem> */}
|
||||
<SelectItem value="review_quality_desc">Quality (High to Low)</SelectItem>
|
||||
<SelectItem value="review_quality_asc">Quality (Low to High)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
|
|
@ -638,33 +692,18 @@ export default function StagingPage() {
|
|||
<TableHead className="w-[18%]">REPOSITORY</TableHead>
|
||||
<TableHead className="text-center">REVIEW</TableHead>
|
||||
<TableHead className="text-center">STATUS</TableHead>
|
||||
{/* COMMENTED OUT: Quality column header with sorting */}
|
||||
<TableHead
|
||||
className="text-center cursor-pointer hover:bg-muted/50"
|
||||
// onClick={() => {
|
||||
// const newSort = sortByFilter.startsWith("review_quality")
|
||||
// ? sortByFilter === "review_quality_desc"
|
||||
// ? "review_quality_asc"
|
||||
// : "review_quality_desc"
|
||||
// : "review_quality_desc"
|
||||
// setSortByFilter(newSort)
|
||||
// }}
|
||||
onClick={() => toggleSort("review_quality")}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span>QUALITY</span>
|
||||
{/* {getSortIcon("review_quality")} */}
|
||||
{getSortIcon("review_quality")}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="text-center cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => {
|
||||
const newSort = sortByFilter.startsWith("speedup_x")
|
||||
? sortByFilter === "speedup_x_desc"
|
||||
? "speedup_x_asc"
|
||||
: "speedup_x_desc"
|
||||
: "speedup_x_desc"
|
||||
setSortByFilter(newSort)
|
||||
}}
|
||||
onClick={() => toggleSort("speedup_x")}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span>SPEEDUP</span>
|
||||
|
|
@ -674,14 +713,7 @@ export default function StagingPage() {
|
|||
<TableHead className="text-center">CHANGES</TableHead>
|
||||
<TableHead
|
||||
className="text-right cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => {
|
||||
const newSort = sortByFilter.startsWith("created_at")
|
||||
? sortByFilter === "created_at_desc"
|
||||
? "created_at_asc"
|
||||
: "created_at_desc"
|
||||
: "created_at_desc"
|
||||
setSortByFilter(newSort)
|
||||
}}
|
||||
onClick={() => toggleSort("created_at")}
|
||||
>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<span>CREATED AT</span>
|
||||
|
|
@ -691,7 +723,9 @@ export default function StagingPage() {
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{events.length === 0 ? (
|
||||
{isLoading ? (
|
||||
<TableSkeleton />
|
||||
) : events.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-12">
|
||||
<div className="text-muted-foreground">
|
||||
|
|
@ -708,7 +742,7 @@ export default function StagingPage() {
|
|||
totalAdditions: 0,
|
||||
totalDeletions: 0,
|
||||
}
|
||||
let coverage: number | null = null
|
||||
|
||||
if (
|
||||
event.metadata &&
|
||||
typeof event.metadata === "object" &&
|
||||
|
|
@ -717,7 +751,6 @@ export default function StagingPage() {
|
|||
event.metadata.diffContents !== null
|
||||
) {
|
||||
const diffContentsRaw = event.metadata.diffContents
|
||||
// Check that every value is an object with oldContent and newContent as strings
|
||||
if (diffContentsRaw && typeof diffContentsRaw === "object") {
|
||||
let valid = true
|
||||
for (const value of Object.values(diffContentsRaw as object)) {
|
||||
|
|
@ -737,19 +770,12 @@ export default function StagingPage() {
|
|||
{ oldContent: string; newContent: string }
|
||||
>
|
||||
diffStats = calculateDiffStats(diffContents)
|
||||
coverage = getCoverageInfo(diffContents)
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallback for coverage if not available
|
||||
if (coverage === null) coverage = 100
|
||||
|
||||
return (
|
||||
<ClickableTableRow
|
||||
key={event.id}
|
||||
event={event}
|
||||
onRowClick={() => handleRowClick(event.trace_id)}
|
||||
>
|
||||
<ClickableTableRow key={event.id} event={event} onRowClick={handleRowClick}>
|
||||
<TableCell className="w-auto min-w-0">
|
||||
<div className="flex items-start gap-3">
|
||||
<FileCode2 className="h-4 w-4 text-muted-foreground mt-1 flex-shrink-0" />
|
||||
|
|
@ -767,7 +793,6 @@ export default function StagingPage() {
|
|||
<div className="flex items-center gap-3">
|
||||
{event.repository ? (
|
||||
<>
|
||||
{/* Repository image */}
|
||||
<div className="relative h-8 w-8 flex-shrink-0">
|
||||
{event.repository.full_name && (
|
||||
<Image
|
||||
|
|
@ -777,13 +802,10 @@ export default function StagingPage() {
|
|||
className="rounded-full object-cover"
|
||||
onError={e => {
|
||||
e.currentTarget.style.display = "none"
|
||||
e.currentTarget.nextElementSibling?.classList.remove("hidden")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Repository name */}
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm font-medium truncate">
|
||||
|
|
@ -794,7 +816,6 @@ export default function StagingPage() {
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Placeholder for untracked optimization */}
|
||||
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
|
||||
<Zap className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
|
@ -856,18 +877,19 @@ export default function StagingPage() {
|
|||
</Table>
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
{/* Pagination */}
|
||||
{!isLoading && totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing {(page - 1) * pageSize + 1} to {Math.min(page * pageSize, totalCount)} of{" "}
|
||||
{totalCount} events
|
||||
Showing {(filters.page - 1) * pageSize + 1} to{" "}
|
||||
{Math.min(filters.page * pageSize, totalCount)} of {totalCount} events
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page === 1}
|
||||
onClick={() => handlePageChange(page - 1)}
|
||||
disabled={filters.page === 1}
|
||||
onClick={() => handlePageChange(filters.page - 1)}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Previous
|
||||
|
|
@ -879,18 +901,18 @@ export default function StagingPage() {
|
|||
|
||||
if (totalPages <= 5) {
|
||||
pageNum = i + 1
|
||||
} else if (page <= 3) {
|
||||
} else if (filters.page <= 3) {
|
||||
pageNum = i + 1
|
||||
} else if (page >= totalPages - 2) {
|
||||
} else if (filters.page >= totalPages - 2) {
|
||||
pageNum = totalPages - 4 + i
|
||||
} else {
|
||||
pageNum = page - 2 + i
|
||||
pageNum = filters.page - 2 + i
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={i}
|
||||
variant={page === pageNum ? "default" : "outline"}
|
||||
variant={filters.page === pageNum ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="w-8 h-8 p-0"
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
|
|
@ -899,8 +921,8 @@ export default function StagingPage() {
|
|||
</Button>
|
||||
)
|
||||
})}
|
||||
{totalPages > 5 && page < totalPages - 2 && <span className="px-2">...</span>}
|
||||
{totalPages > 5 && page < totalPages - 2 && (
|
||||
{totalPages > 5 && filters.page < totalPages - 2 && <span className="px-2">...</span>}
|
||||
{totalPages > 5 && filters.page < totalPages - 2 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
@ -915,8 +937,8 @@ export default function StagingPage() {
|
|||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page === totalPages}
|
||||
onClick={() => handlePageChange(page + 1)}
|
||||
disabled={filters.page === totalPages}
|
||||
onClick={() => handlePageChange(filters.page + 1)}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue