diff --git a/.github/workflows/chrome-extension-ci.yml b/.github/workflows/chrome-extension-ci.yml index 93c21cc..4f71459 100644 --- a/.github/workflows/chrome-extension-ci.yml +++ b/.github/workflows/chrome-extension-ci.yml @@ -75,7 +75,7 @@ jobs: # Don't increment major versions (x.0.0) echo "version=$current_version" >> $GITHUB_OUTPUT echo "version_changed=false" >> $GITHUB_OUTPUT - elif git ls-remote --tags origin refs/tags/v$current_version >/dev/null; then + elif git ls-remote --tags origin | grep -q "refs/tags/v$current_version"; then # If tag exists and it's not a major version, increment patch IFS='.' read -r major minor patch <<< "$current_version" new_version="$major.$minor.$((patch + 1))" diff --git a/extensions/changelog.md b/extensions/changelog.md index 323b129..dc57f7c 100644 --- a/extensions/changelog.md +++ b/extensions/changelog.md @@ -1,5 +1,27 @@ ## Changelog of the extensions +### feat/fix/ref: - 11/27/2024 - @lumpinif + +#### Performance & Code Quality + +- Extremely streamline code structure and implementation approach +- Much optimized performance +- Streamline and organize code for thinking-block implementation + +#### Bug Fixes + +- Fix flash of unstyled content (FOUC) +- Fix stutter when submitting new replies +- Fix FOUC and streaming issues for thinking-block implementation + +#### UI Improvements + +- Update chevron icon with transition effect + +#### Architecture + +- Implement ultimate approach with simplest and most effective implementation after experimentation + ### fix: - 11/17/2024 - @lumpinif #### Observer Management and Memory Leak Prevention diff --git a/extensions/chrome/README.md b/extensions/chrome/README.md index 7b0cc45..3efce83 100644 --- a/extensions/chrome/README.md +++ b/extensions/chrome/README.md @@ -223,6 +223,23 @@ We use several tools to maintain code quality: - **ESLint**: Finds and fixes JavaScript problems - **Prettier**: Formats your code consistently +### Version Control & Releases + +The project uses automated version bumping through CI: + +- **Automatic Version Bumping**: When code is merged to main, the CI will: + + - Auto-increment the patch version (e.g., 1.0.0 -> 1.0.1) + - Create a new release with the bumped version + - Skip version bump for major versions (x.0.0) + +- **Manual Version Control**: + - Developers can manually set both versions in `package.json` and `manifest.json` + - Major version changes (x.0.0) must be set manually + - Manual versions will be respected by CI + +> **Note**: If you need to manually set a version, update both `package.json` and `manifest.json` before merging to main. + ### Continuous Integration Our GitHub Actions setup automatically: diff --git a/extensions/chrome/components.json b/extensions/chrome/components.json index 23aac84..907cbba 100644 --- a/extensions/chrome/components.json +++ b/extensions/chrome/components.json @@ -4,7 +4,7 @@ "rsc": false, "tsx": true, "tailwind": { - "config": "tailwind.config.js", + "config": "tailwind.config.cjs", "css": "src/styles/globals.css", "baseColor": "zinc", "cssVariables": true, diff --git a/extensions/chrome/eslint.config.cjs b/extensions/chrome/eslint.config.cjs index 640b2bc..69cf971 100644 --- a/extensions/chrome/eslint.config.cjs +++ b/extensions/chrome/eslint.config.cjs @@ -33,6 +33,9 @@ module.exports = [ setInterval: "readonly", Node: "readonly", HTMLButtonElement: "readonly", + MutationRecord: "readonly", + MouseEvent: "readonly", + SVGSVGElement: "readonly", }, }, plugins: { @@ -58,6 +61,8 @@ module.exports = [ "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", "no-inline-styles": "off", + "no-undef": "off", + "react/prop-types": "off", }, settings: { react: { diff --git a/extensions/chrome/package.json b/extensions/chrome/package.json index 7c1de56..7e2da8f 100644 --- a/extensions/chrome/package.json +++ b/extensions/chrome/package.json @@ -1,6 +1,6 @@ { "name": "thinking-claude", - "version": "3.0.0", + "version": "3.1.0", "description": "Chrome extension for letting Claude think like a real human", "type": "module", "scripts": { @@ -19,6 +19,12 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@radix-ui/react-accordion": "^1.2.1", + "@radix-ui/react-collapsible": "^1.1.1", + "@radix-ui/react-scroll-area": "^1.2.1", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", + "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.460.0", "react": "^18.2.0", @@ -46,6 +52,7 @@ "identity-obj-proxy": "^3.0.0", "lint-staged": "^14.0.1", "markdownlint-cli2": "^0.15.0", + "mini-css-extract-plugin": "^2.9.2", "postcss": "^8.4.31", "postcss-loader": "^7.3.3", "postcss-nesting": "^12.0.1", diff --git a/extensions/chrome/postcss.config.mjs b/extensions/chrome/postcss.config.cjs similarity index 74% rename from extensions/chrome/postcss.config.mjs rename to extensions/chrome/postcss.config.cjs index 2aa7205..33ad091 100644 --- a/extensions/chrome/postcss.config.mjs +++ b/extensions/chrome/postcss.config.cjs @@ -1,6 +1,6 @@ -export default { +module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, -}; +} diff --git a/extensions/chrome/public/manifest.json b/extensions/chrome/public/manifest.json index a73e4eb..f01c2df 100644 --- a/extensions/chrome/public/manifest.json +++ b/extensions/chrome/public/manifest.json @@ -1,12 +1,13 @@ { "manifest_version": 3, "name": "Thinking Claude", - "version": "3.0.0", + "version": "3.1.0", "description": "Chrome extension for letting Claude think like a real human", "content_scripts": [ { "matches": ["https://*.claude.ai/*"], - "js": ["content.js"] + "js": ["content.js"], + "css": ["styles.css"] } ], "icons": { diff --git a/extensions/chrome/public/styles.css b/extensions/chrome/public/styles.css new file mode 100644 index 0000000..d2e6d5d --- /dev/null +++ b/extensions/chrome/public/styles.css @@ -0,0 +1,188 @@ +/* Overwrite original claude thinking block content styles */ +.code-block__code { + background: none !important; + white-space: pre-wrap !important; + word-wrap: break-word !important; + overflow-y: auto !important; + overflow-x: hidden !important; + color: var(--text-text-200) !important; + padding: 1em !important; + max-width: 100%; + display: block; + height: auto !important; + min-height: 0vh !important; + max-height: 50vh !important; + visibility: visible !important; + opacity: 1 !important; + transition: all 0.3s ease-out !important; +} + +/* collapsed states */ + +/* Collapsed state */ +.code-block__code.collapsed { + height: 0 !important; + padding: 0 !important; + visibility: hidden !important; + opacity: 0 !important; +} + +/* Collapsed state */ +[data-thinking-block-state="collapsed"] .code-block__code { + height: 0 !important; + padding: 0 !important; + visibility: hidden !important; + opacity: 0 !important; +} + +/* Expanded state */ +/* [data-thinking-block-state="expanded"] .code-block__code { + height: 50vh !important; + padding: 1em !important; + visibility: visible !important; + opacity: 1 !important; +} */ + +code { + background: none !important; + white-space: pre-wrap !important; + word-wrap: break-word !important; + text-wrap: balance !important; + color: hsl(var(--text-300)) !important; + font-size: 0.875rem !important; + display: block !important; + max-width: 100% !important; + text-shadow: none !important; +} + +/* Add selection styles */ +code span::selection { + background: hsl(var(--clay) / var(--tw-text-opacity)) !important; + color: hsl(var(--text-100)) !important; +} + +code span::-moz-selection { + background: hsl(var(--clay) / var(--tw-text-opacity)) !important; + color: hsl(var(--text-100)) !important; +} + +code span:hover { + transition: color 0.4s ease; + color: hsl(var(--text-100)); +} + +/* --------------------------------- */ + +/* Copy button container */ +div[data-is-streaming] .pointer-events-none.sticky { + cursor: pointer !important; + pointer-events: auto !important; +} + +/* Copy button container */ +div[data-is-streaming] .from-bg-300\\/90 { + pointer-events: auto !important; + user-select: none !important; +} + +/* --------------------------------- */ + +/* Update the header text */ +/* This is the original header text */ +pre .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty) { + font-size: 0; /* Hide original text */ + pointer-events: auto !important; /* Make sure it's clickable */ + cursor: pointer !important; + display: inline-flex; + align-items: center; + font-family: var(--font-user-message); +} + +/* Update the text of the header */ +[data-thinking-block-state="collapsed"] + pre + .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty)::after { + content: "Open Claude's thinking"; + font-size: 0.875rem; /* Restore font size */ + cursor: pointer; + font-family: var(--font-user-message); + transition: color 0.15s ease-in-out; +} + +pre .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty)::after { + content: "Claude's thinking"; + font-size: 0.875rem; /* Restore font size */ + cursor: pointer; + font-family: var(--font-user-message); + transition: color 0.15s ease-in-out; +} + +/* Hover state */ +[data-thinking-block-state="expanded"] + pre + .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty):hover::after { + color: hsl(var(--text-100)); + content: "Hide Claude's thinking"; +} + +/* Streaming state styles */ +div[data-is-streaming="true"] + pre + .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty)::after { + content: "Claude is thinking..."; + background: linear-gradient( + 90deg, + rgba(156, 163, 175, 0.7) 0%, + rgba(209, 213, 219, 1) 25%, + rgba(156, 163, 175, 0.7) 50%, + rgba(209, 213, 219, 1) 75%, + rgba(156, 163, 175, 0.7) 100% + ); + background-size: 200% 100%; + animation: gradientWave 3s linear infinite; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + color: transparent; + cursor: pointer; + font-family: var(--font-user-message); +} + +/* Chevron-down icon */ +pre .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty)::before { + content: ""; + width: 15px; + height: 15px; + margin-right: 0.25rem; + -webkit-mask-image: url('data:image/svg+xml;utf8,'); + mask-image: url('data:image/svg+xml;utf8,'); + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background-color: hsl(var(--text-500)); + transform: translateY(-1px); + transition: transform 0.25s ease-out; +} + +/* --------------------------------- */ + +/* Shimmer animation for streaming state */ +@keyframes gradientWave { + 0% { + background-position: 200% 50%; + } + 100% { + background-position: -200% 50%; + } +} + +/* --------------------------------- */ + +/* Chevron animation */ + +[data-thinking-block-state="collapsed"] + pre + .text-text-300.absolute.pl-3.pt-2\.5.text-xs:not(:empty)::before { + transform: rotate(180deg); +} diff --git a/extensions/chrome/src/components/sample-ui.tsx b/extensions/chrome/src/components/sample-ui.tsx new file mode 100644 index 0000000..4201e62 --- /dev/null +++ b/extensions/chrome/src/components/sample-ui.tsx @@ -0,0 +1,9 @@ +import React from "react" + +export const SampleApp: React.FC = () => { + return ( +
+
Hello World
+
+ ) +} diff --git a/extensions/chrome/src/components/ui/accordion.tsx b/extensions/chrome/src/components/ui/accordion.tsx new file mode 100644 index 0000000..58be9bd --- /dev/null +++ b/extensions/chrome/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" + +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/extensions/chrome/src/components/ui/button.tsx b/extensions/chrome/src/components/ui/button.tsx new file mode 100644 index 0000000..e70c05c --- /dev/null +++ b/extensions/chrome/src/components/ui/button.tsx @@ -0,0 +1,58 @@ +import * as React from "react" + +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-0 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/extensions/chrome/src/components/ui/collapsible.tsx b/extensions/chrome/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..a23e7a2 --- /dev/null +++ b/extensions/chrome/src/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/extensions/chrome/src/components/ui/scroll-area.tsx b/extensions/chrome/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..8bb00b6 --- /dev/null +++ b/extensions/chrome/src/components/ui/scroll-area.tsx @@ -0,0 +1,47 @@ +import * as React from "react" + +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/extensions/chrome/src/components/ui/skeleton.tsx b/extensions/chrome/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..d7e45f7 --- /dev/null +++ b/extensions/chrome/src/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/extensions/chrome/src/components/ui/tooltip.tsx b/extensions/chrome/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..79ce777 --- /dev/null +++ b/extensions/chrome/src/components/ui/tooltip.tsx @@ -0,0 +1,31 @@ +import * as React from "react" + +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/extensions/chrome/src/constants/constants.ts b/extensions/chrome/src/constants/constants.ts index c4183b9..e69de29 100644 --- a/extensions/chrome/src/constants/constants.ts +++ b/extensions/chrome/src/constants/constants.ts @@ -1,94 +0,0 @@ -import { Icons, Selectors, Styles, Timings } from "@/types" - -export const selectors: Selectors = { - // Using data attribute for message container - messageContainer: "[data-is-streaming]", - - // Using combination of class and content for thinking label - thinkingLabel: 'div.text-xs:has(text="thinking")', - - // Using class and language attribute for code block - code: 'code[class*="language-thinking"]', - - // Using specific class combinations and structure - codeContainer: ".relative.flex.flex-col.rounded-lg", - - // Using specific pre within grid structure - pre: ".grid-cols-1.grid > pre", - - // Using specific class and container structure - thinkingProcess: '.code-block__code:has(>code[class*="language-thinking"])', -} - -export const timings: Timings = { - retryDelay: 1000, - mutationDelay: 100, - checkInterval: 2000, - copyFeedback: 2000, - maxRetries: 10, -} - -export const icons: Icons = { - arrow: ``, - tick: ``, - copy: ``, -} - -export const styles: Styles = { - animation: ` - @keyframes gradientWave { - 0% { background-position: 200% 50%; } - 100% { background-position: -200% 50%; } - } - - .thinking-animation { - background: linear-gradient( - 90deg, - rgba(156, 163, 175, 0.7) 0%, - rgba(209, 213, 219, 1) 25%, - rgba(156, 163, 175, 0.7) 50%, - rgba(209, 213, 219, 1) 75%, - rgba(156, 163, 175, 0.7) 100% - ); - background-size: 200% 100%; - animation: gradientWave 3s linear infinite; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - color: transparent; - } - - .thinking-header { - user-select: none; - display: flex; - align-items: center; - gap: 8px; - padding: 8px; - background-color: rgb(40, 44, 52); - border-radius: 6px 6px 0 0; - } - - .thinking-content { - transition: all 0.3s ease-in-out; - overflow-x: hidden; - overflow-y: auto; - max-height: 0; - opacity: 0; - padding: 0; - max-width: 100%; - display: block; - background-color: rgb(40, 44, 52); - border-radius: 0 0 6px 6px; - } - - .thinking-content code { - white-space: pre-wrap !important; - word-break: break-word !important; - overflow-wrap: break-word !important; - display: block !important; - max-width: 100% !important; - } - `, - buttonClass: "flex items-center text-text-500 hover:text-text-300", - labelClass: "font-medium text-sm", -} diff --git a/extensions/chrome/src/content/index.ts b/extensions/chrome/src/content/index.ts new file mode 100644 index 0000000..167eb16 --- /dev/null +++ b/extensions/chrome/src/content/index.ts @@ -0,0 +1,14 @@ +import "@/styles/globals.css" + +import { shouldInitialize } from "@/utils/url-utils" + +import { addThinkingBlockToggle } from "./v3/features/thinking-block" + +// Only initialize on appropriate pages +if (shouldInitialize(window.location.href)) { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", addThinkingBlockToggle) + } else { + addThinkingBlockToggle() + } +} diff --git a/extensions/chrome/src/content/index.tsx b/extensions/chrome/src/content/index.tsx deleted file mode 100644 index 124e7d3..0000000 --- a/extensions/chrome/src/content/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ThinkingBlockManager } from "./managers/thinking-block-manager" - -/** - * Check if current URL matches Claude chat pattern - */ -const isChatURL = (): boolean => { - const url = window.location.href - return url.startsWith("https://claude.ai/chat/") -} - -/** - * Initialize the extension only on Claude chat pages - */ -const init = (): void => { - if (!isChatURL()) return - - const manager = new ThinkingBlockManager() - manager.init() -} - -// Initialize when DOM is ready -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", init) -} else { - init() -} diff --git a/extensions/chrome/src/content/managers/README.md b/extensions/chrome/src/content/managers/README.md deleted file mode 100644 index 1a69cd6..0000000 --- a/extensions/chrome/src/content/managers/README.md +++ /dev/null @@ -1,133 +0,0 @@ -# Thinking Process Manager Architecture - -This document outlines the architecture and workflow of the Thinking Process visualization feature in the Chrome extension. - -## Architecture Overview - -```mermaid -flowchart TB - subgraph Extension["Chrome Extension Content Script"] - Entry["index.tsx\nEntry Point"] - end - - subgraph ThinkingBlock["ThinkingBlockManager"] - direction TB - Init["Initialize"] - Process["Process Block"] - Cleanup["Cleanup Resources"] - end - - subgraph Managers["Manager Classes"] - direction LR - DOM["DOMObserverManager\n- Watch for new blocks\n- Periodic checks"] - UI["UIComponentManager\n- Create UI elements\n- Style components"] - Event["EventManager\n- Handle interactions\n- Manage UI state"] - Style["StyleManager\n- Inject styles\n- Manage animations"] - end - - Entry --> Init - Init --> Style - Init --> DOM - DOM --> Process - Process --> UI - Process --> Event - Event --> UI -``` - -## Component Workflow - -```mermaid -sequenceDiagram - participant Entry as index.tsx - participant TBM as ThinkingBlockManager - participant DOM as DOMObserverManager - participant UI as UIComponentManager - participant Event as EventManager - participant Style as StyleManager - - Entry->>TBM: Initialize - TBM->>Style: Inject Styles - TBM->>DOM: Initialize Observer - DOM->>DOM: Setup Periodic Check - - loop DOM Observation - DOM->>TBM: Process New Block - TBM->>UI: Create UI Components - TBM->>Event: Setup Event Handlers - Event-->>UI: Update UI State - end - - Note over TBM,Event: User Interactions - Event->>Event: Handle Toggle - Event->>Event: Handle Copy -``` - -## Component Responsibilities - -### ThinkingBlockManager - -- Central coordinator for the thinking process feature -- Initializes and manages other components -- Processes new thinking blocks -- Handles cleanup on unload - -### DOMObserverManager - -- Observes DOM for new thinking blocks -- Performs periodic checks for missed blocks -- Manages retry mechanism for initialization -- Handles cleanup of observers - -### UIComponentManager - -- Creates UI elements (buttons, containers) -- Applies consistent styling -- Manages component hierarchy -- Handles component updates - -### EventManager - -- Sets up event listeners -- Manages UI state transitions -- Handles copy functionality -- Provides user feedback - -### StyleManager - -- Injects required styles -- Manages animation styles -- Ensures single style injection -- Handles style cleanup - -## User Interaction Flow - -```mermaid -stateDiagram-v2 - [*] --> Hidden: Initial State - Hidden --> Visible: Click Toggle - Visible --> Hidden: Click Toggle - Visible --> Copied: Click Copy - Copied --> Visible: After Feedback - Hidden --> [*]: Cleanup - Visible --> [*]: Cleanup -``` - -## Installation and Usage - -The Thinking Process Manager is automatically initialized when the Chrome extension loads. It requires no manual setup and begins observing for thinking process blocks immediately. - -## Development - -To modify or extend the functionality: - -1. Each manager is designed to be independent and focused on a single responsibility -2. New features should be added to the appropriate manager -3. The ThinkingBlockManager coordinates all interactions between managers -4. Follow the established TypeScript types and interfaces - -## Error Handling - -- DOM observation includes retry mechanism -- Event handlers include error prevention -- Style injection prevents duplicates -- All cleanup is handled automatically diff --git a/extensions/chrome/src/content/managers/thinking-block-manager/events/event-manager.ts b/extensions/chrome/src/content/managers/thinking-block-manager/events/event-manager.ts deleted file mode 100644 index 702883c..0000000 --- a/extensions/chrome/src/content/managers/thinking-block-manager/events/event-manager.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { icons, timings } from "@/constants/constants" - -/** - * Manages event handling and UI state updates for thinking process blocks. - * Handles button click events, copy functionality, and UI state transitions. - */ -export class EventManager { - /** - * Sets up the toggle button click handler. - * @param toggleBtn - The button element that toggles the thinking process visibility - * @param container - The container element that holds the thinking process content - * @param updateUIState - Callback function to update the UI state - */ - static setupToggleButton( - toggleBtn: HTMLButtonElement, - container: HTMLElement, - updateUIState: (isOpen: boolean) => void - ): void { - toggleBtn.addEventListener("click", (e) => { - e.stopPropagation() - const currentState = container.dataset.isOpen === "true" - updateUIState(!currentState) - }) - } - - /** - * Sets up the copy button functionality. - * Handles click events, clipboard operations, and feedback animations. - * @param copyBtn - The button element that triggers the copy operation - * @param content - The element containing the content to be copied - */ - static setupCopyButton( - copyBtn: HTMLButtonElement, - content: Element | null - ): void { - copyBtn.addEventListener("click", (e) => { - e.stopPropagation() - if (content && !copyBtn.disabled) { - copyBtn.disabled = true - copyBtn.style.opacity = "0.5" - copyBtn.style.cursor = "default" - navigator.clipboard.writeText(content.textContent || "") - - const copyBtnText = copyBtn.querySelector("span.text-text-200") - const copyIcon = copyBtn.querySelector("span:first-child") - - if (copyBtnText) copyBtnText.textContent = "Copied!" - if (copyIcon) copyIcon.innerHTML = icons.tick - - setTimeout(() => { - if (copyBtnText) copyBtnText.textContent = "Copy" - if (copyIcon) copyIcon.innerHTML = icons.copy - copyBtn.disabled = false - copyBtn.style.opacity = "" - copyBtn.style.cursor = "" - }, timings.copyFeedback) - } - }) - } - - /** - * Updates the UI state of a thinking process block. - * Handles animations, text changes, and visibility states. - * @param container - The container element to update - * @param toggleBtn - The toggle button element to update - * @param isOpen - Whether the thinking process should be visible - */ - static updateUIState( - container: HTMLElement, - toggleBtn: HTMLButtonElement, - isOpen: boolean - ): void { - container.dataset.isOpen = isOpen.toString() - const arrow = toggleBtn.querySelector("svg") - const label = toggleBtn.querySelector("span") - - container.style.maxHeight = isOpen ? "50vh" : "0" - container.style.opacity = isOpen ? "1" : "0" - container.style.padding = isOpen ? "1em" : "0" - - if (label) { - label.textContent = isOpen - ? "Hide thinking process" - : "View thinking process" - } - - if (arrow) { - arrow.style.transform = `rotate(${isOpen ? 180 : 0}deg)` - } - } -} diff --git a/extensions/chrome/src/content/managers/thinking-block-manager/index.ts b/extensions/chrome/src/content/managers/thinking-block-manager/index.ts deleted file mode 100644 index 2b20f10..0000000 --- a/extensions/chrome/src/content/managers/thinking-block-manager/index.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { selectors } from "@/constants/constants" - -import { EventManager } from "./events/event-manager" -import { DOMObserverManager } from "./observer/dom-observer-manager" -import { StyleManager } from "./styles/style-manager" -import { UIComponentManager } from "./ui/ui-component-manager" - -/** - * Manages the thinking process visualization feature in Claude's interface. - * Coordinates between UI components, DOM observation, events, and styles - * to create and maintain interactive thinking process blocks. - */ -export class ThinkingBlockManager { - private domObserver: DOMObserverManager - - /** - * Initializes the ThinkingBlockManager instance. - * Injects styles, sets up DOM observation, and adds an event listener for cleanup. - */ - constructor() { - StyleManager.injectStyles() - this.domObserver = new DOMObserverManager(this.processBlock.bind(this)) - this.domObserver.initWithRetry() - - window.addEventListener("unload", () => this.cleanupResources()) - } - - /** - * Processes a code block element to transform it into an interactive thinking process block. - * Creates and sets up the toggle button, copy functionality, and container styling. - * @param pre - The pre element containing the thinking process code - */ - private processBlock(pre: HTMLElement): void { - const container = pre.querySelector(selectors.code)?.parentElement - if (!container) return - - const outerDiv = pre.querySelector(selectors.codeContainer) - if (!outerDiv) return - - while (outerDiv.firstChild) { - outerDiv.removeChild(outerDiv.firstChild) - } - - container.dataset.isOpen = "true" - - const toggleBtn = UIComponentManager.createToggleButton() - const copyBtn = UIComponentManager.createCopyButton() - const header = UIComponentManager.createHeader(toggleBtn, copyBtn) - - UIComponentManager.setupContainer(container) - - outerDiv.appendChild(header) - outerDiv.appendChild(container) - - const updateUIState = (isOpen: boolean) => { - EventManager.updateUIState(container, toggleBtn, isOpen) - } - - EventManager.setupToggleButton(toggleBtn, container, updateUIState) - EventManager.setupCopyButton( - copyBtn, - container.querySelector(selectors.code) - ) - } - - /** - * Cleans up resources when the component is being destroyed. - * Disconnects DOM observers and removes event listeners. - */ - private cleanupResources(): void { - this.domObserver.cleanupObservers() - } - - // Add this method to resolve TypeScript error - init(): void { - // No additional initialization needed, as constructor handles it - } -} diff --git a/extensions/chrome/src/content/managers/thinking-block-manager/observer/dom-observer-manager.ts b/extensions/chrome/src/content/managers/thinking-block-manager/observer/dom-observer-manager.ts deleted file mode 100644 index 1e5aaa6..0000000 --- a/extensions/chrome/src/content/managers/thinking-block-manager/observer/dom-observer-manager.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { timings } from "@/constants/constants" - -/** - * Manages DOM observation for thinking process blocks. - * Watches for new code blocks being added to the page and triggers processing. - * Also performs periodic checks for any blocks that might have been missed. - */ -export class DOMObserverManager { - private observers: Set - private processBlock: (pre: HTMLElement) => void - - /** - * Creates a new DOMObserverManager instance. - * @param processBlock - Callback function to process newly found code blocks - */ - constructor(processBlock: (pre: HTMLElement) => void) { - this.observers = new Set() - this.processBlock = processBlock - } - - /** - * Initializes the DOM observer with retry capability. - * Will attempt to retry setup if it fails, up to a maximum number of retries. - * @param retryCount - Current number of retry attempts - */ - initWithRetry(retryCount = 0): void { - try { - this.setupObserver() - } catch (error) { - console.error(error) - if (retryCount < timings.maxRetries) { - setTimeout(() => this.initWithRetry(retryCount + 1), timings.retryDelay) - } - } - } - - /** - * Cleans up all observers by disconnecting them and clearing the set. - */ - cleanupObservers(): void { - this.observers.forEach((observer) => observer.disconnect()) - this.observers.clear() - } - - /** - * Sets up the mutation observer to watch for DOM changes. - * Observes the entire document body for added nodes that might contain code blocks. - */ - private setupObserver(): void { - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if (node instanceof HTMLElement) { - const pre = node.matches("pre") ? node : node.querySelector("pre") - if (pre) { - setTimeout(() => this.processBlock(pre), timings.mutationDelay) - } - } - }) - }) - }) - - observer.observe(document.body, { - childList: true, - subtree: true, - }) - - this.observers.add(observer) - this.setupPeriodicBlockCheck() - } - - /** - * Sets up periodic checking for code blocks that might have been missed. - * Runs at regular intervals defined in timing configuration. - */ - private setupPeriodicBlockCheck(): void { - setInterval(() => { - document.querySelectorAll("pre").forEach((pre) => { - if (!pre.querySelector(".thinking-header")) { - this.processBlock(pre as HTMLElement) - } - }) - }, timings.checkInterval) - } -} diff --git a/extensions/chrome/src/content/managers/thinking-block-manager/styles/style-manager.ts b/extensions/chrome/src/content/managers/thinking-block-manager/styles/style-manager.ts deleted file mode 100644 index c3c4eec..0000000 --- a/extensions/chrome/src/content/managers/thinking-block-manager/styles/style-manager.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { styles } from "@/constants/constants" - -/** - * Manages the injection and removal of styles for thinking process blocks. - * Handles animation styles and ensures they are only injected once. - */ -export class StyleManager { - /** - * Injects the required styles for thinking process animations into the document head. - * Checks if styles are already present to avoid duplicate injection. - */ - static injectStyles(): void { - if (!document.getElementById("thinking-animation-styles")) { - const styleSheet = document.createElement("style") - styleSheet.id = "thinking-animation-styles" - styleSheet.textContent = styles.animation - document.head.appendChild(styleSheet) - } - } -} diff --git a/extensions/chrome/src/content/managers/thinking-block-manager/ui/ui-component-manager.ts b/extensions/chrome/src/content/managers/thinking-block-manager/ui/ui-component-manager.ts deleted file mode 100644 index f00d9ad..0000000 --- a/extensions/chrome/src/content/managers/thinking-block-manager/ui/ui-component-manager.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { icons, selectors, styles } from "@/constants/constants" - -/** - * Manages the creation and styling of UI components for thinking process blocks. - * Provides factory methods for creating buttons, headers, and containers with consistent styling. - */ -export class UIComponentManager { - /** - * Creates a DOM element with optional class name and inner HTML. - * @param tag - The HTML tag name for the element - * @param className - Optional CSS class names to add - * @param innerHTML - Optional inner HTML content - * @returns The created HTML element - */ - static createElement( - tag: string, - className: string = "", - innerHTML: string = "" - ): HTMLElement { - const element = document.createElement(tag) - if (className) element.className = className - if (innerHTML) element.innerHTML = innerHTML - return element - } - - /** - * Creates a toggle button for showing/hiding thinking process content. - * @param isStreaming - Whether Claude is currently streaming a response - * @returns A styled button element with appropriate text and icon - */ - static createToggleButton(isStreaming = false): HTMLButtonElement { - const button = this.createElement( - "button", - "flex items-center text-text-500 hover:text-text-300" - ) - const labelText = isStreaming - ? "Claude is Thinking..." - : "Hide thinking process" - button.innerHTML = ` - ${icons.arrow} - ${labelText} - ` - return button as HTMLButtonElement - } - - /** - * Creates a copy button with appropriate styling and icon. - * @returns A styled button element for copying content - */ - static createCopyButton(): HTMLButtonElement { - const copyBtnContainer = this.createElement( - "div", - "from-bg-300/90 to-bg-300/70 pointer-events-auto rounded-md bg-gradient-to-b p-0.5 backdrop-blur-md" - ) - - const copyBtn = this.createElement( - "button", - "flex flex-row items-center gap-1 rounded-md p-1 py-0.5 text-xs transition-opacity delay-100 hover:bg-bg-200 opacity-60 hover:opacity-100" - ) as HTMLButtonElement - - const copyIcon = this.createElement("span", "", icons.copy) - const copyBtnText = this.createElement( - "span", - "text-text-200 pr-0.5", - "Copy" - ) - - copyBtn.appendChild(copyIcon) - copyBtn.appendChild(copyBtnText) - copyBtnContainer.appendChild(copyBtn) - - return copyBtn - } - - /** - * Creates a header element containing toggle and copy buttons. - * @param toggleBtn - The toggle button element - * @param copyBtn - The copy button element - * @returns A styled header element containing the buttons - */ - static createHeader( - toggleBtn: HTMLButtonElement, - copyBtn: HTMLButtonElement - ): HTMLElement { - const header = this.createElement("div", "thinking-header", "") - header.style.cssText = - "display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: var(--bg-300);" - - header.appendChild(toggleBtn) - header.appendChild(copyBtn.parentElement!) - - return header - } - - /** - * Sets up the container element with appropriate styling and classes. - * @param container - The container element to style - */ - static setupContainer(container: HTMLElement): void { - container.className = - "code-block__code !my-0 !rounded-lg !text-sm !leading-relaxed" - container.style.cssText = - "transition: 0.3s ease-in-out; overflow: hidden auto; max-height: 50vh; opacity: 1; padding: 1em; max-width: 100%; display: block;" - - const content = container.querySelector(selectors.code) - if (content instanceof HTMLElement) { - content.style.cssText = - "white-space: pre-wrap !important; word-break: break-word !important; overflow-wrap: break-word !important; display: block !important; max-width: 100% !important;" - } - } -} diff --git a/extensions/chrome/src/content/v3/features/thinking-block/index.ts b/extensions/chrome/src/content/v3/features/thinking-block/index.ts new file mode 100644 index 0000000..cfadc24 --- /dev/null +++ b/extensions/chrome/src/content/v3/features/thinking-block/index.ts @@ -0,0 +1 @@ +export { addThinkingBlockToggle } from "./thinking-block-toggle" diff --git a/extensions/chrome/src/content/v3/features/thinking-block/setup-controls.ts b/extensions/chrome/src/content/v3/features/thinking-block/setup-controls.ts new file mode 100644 index 0000000..a48e33a --- /dev/null +++ b/extensions/chrome/src/content/v3/features/thinking-block/setup-controls.ts @@ -0,0 +1,59 @@ +export function setupControls( + control: HTMLElement, + thinkingBlock: Element, + resContainer: Element +) { + const copyButton = control.querySelector("button") + + if (!copyButton) return + copyButton.classList.add("tc-select-none") + copyButton.addEventListener( + "click", + async (e) => { + e.stopPropagation() + await handleCopyClick(copyButton, thinkingBlock) + }, + true + ) + + control.addEventListener("click", () => { + const currentState = resContainer.getAttribute("data-thinking-block-state") + + // Update the collapse state of thinking block in the response container + const newState = currentState === "expanded" ? "collapsed" : "expanded" + resContainer.setAttribute("data-thinking-block-state", newState) + + // Toggle the collapse state of the thinking block as fallback + thinkingBlock.classList.toggle("collapsed") + }) +} + +async function handleCopyClick(copyButton: Element, codeBlock: Element) { + const codeElement = codeBlock.querySelector("code") + if (!codeElement) return + + try { + await navigator.clipboard.writeText(codeElement.textContent || "") + updateCopyButtonUI(copyButton) + } catch (err) { + console.error("[TC] Failed to copy:", err) + } +} + +function updateCopyButtonUI(copyButton: Element) { + const textSpan = copyButton.querySelector("span") + const svg = copyButton.querySelector("svg") + if (!textSpan || !svg) return + + const originalText = textSpan.textContent + const originalSvgPath = svg.innerHTML + + textSpan.textContent = "Copied" + svg.innerHTML = + '' + + setTimeout(() => { + textSpan.textContent = originalText + svg.innerHTML = originalSvgPath + }, 2000) +} diff --git a/extensions/chrome/src/content/v3/features/thinking-block/thinking-block-toggle.ts b/extensions/chrome/src/content/v3/features/thinking-block/thinking-block-toggle.ts new file mode 100644 index 0000000..9f34877 --- /dev/null +++ b/extensions/chrome/src/content/v3/features/thinking-block/thinking-block-toggle.ts @@ -0,0 +1,34 @@ +import { THINKING_BLOCK_CONTROLS_SELECTORS } from "@/selectors" +import { mutationObserver } from "@/services/mutation-observer" + +import { setupControls } from "./setup-controls" + +export function addThinkingBlockToggle() { + mutationObserver.initialize() + mutationObserver.subscribe(processThinkingBlocks) +} + +function processThinkingBlocks() { + const thinkingBlockControls = document.querySelectorAll( + THINKING_BLOCK_CONTROLS_SELECTORS + ) + + thinkingBlockControls.forEach(processControl) +} + +function processControl(control: Element) { + if (control.hasAttribute("data-tc-processed")) return + control.setAttribute("data-tc-processed", "true") + + const resContainer = control.closest("div[data-is-streaming]") as HTMLElement + const thinkingBlock = control + .closest("pre") + ?.querySelector(".code-block__code") + if (!thinkingBlock) return + + if (!resContainer.hasAttribute("data-thinking-block-state")) { + resContainer.setAttribute("data-thinking-block-state", "expanded") + } + + setupControls(control as HTMLElement, thinkingBlock, resContainer) +} diff --git a/extensions/chrome/src/selectors/index.ts b/extensions/chrome/src/selectors/index.ts new file mode 100644 index 0000000..ffd0348 --- /dev/null +++ b/extensions/chrome/src/selectors/index.ts @@ -0,0 +1,4 @@ +export const THINKING_BLOCK_CONTROLS_SELECTORS = [ + ".text-text-300.absolute", + ".pointer-events-none.sticky", +].join(", ") diff --git a/extensions/chrome/src/services/mutation-observer.ts b/extensions/chrome/src/services/mutation-observer.ts new file mode 100644 index 0000000..efc8c29 --- /dev/null +++ b/extensions/chrome/src/services/mutation-observer.ts @@ -0,0 +1,52 @@ +type ObserverCallback = () => void + +class MutationObserverService { + private observer: MutationObserver | null = null + private callbacks: Set = new Set() + private timeouts: Map = new Map() + private isProcessing = false + + initialize() { + if (this.observer) return + + this.observer = new MutationObserver(() => { + if (this.isProcessing) return + + this.isProcessing = true + this.callbacks.forEach((callback) => { + const existingTimeout = this.timeouts.get(callback) + if (existingTimeout) { + clearTimeout(existingTimeout) + } + + const timeout = setTimeout(() => { + callback() + this.isProcessing = false + }, 200) // Slightly increased debounce time + + this.timeouts.set(callback, timeout) + }) + }) + + this.observer.observe(document.body, { + childList: true, + subtree: true, + }) + } + + subscribe(callback: ObserverCallback) { + this.callbacks.add(callback) + return () => this.unsubscribe(callback) + } + + private unsubscribe(callback: ObserverCallback) { + this.callbacks.delete(callback) + const timeout = this.timeouts.get(callback) + if (timeout) { + clearTimeout(timeout) + this.timeouts.delete(callback) + } + } +} + +export const mutationObserver = new MutationObserverService() diff --git a/extensions/chrome/src/styles/globals.css b/extensions/chrome/src/styles/globals.css index d8fade3..63d0cf3 100644 --- a/extensions/chrome/src/styles/globals.css +++ b/extensions/chrome/src/styles/globals.css @@ -4,87 +4,47 @@ @layer base { :root { - --background: 0 0% 100%; - --foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 47.4% 11.2%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --card: 0 0% 100%; - --card-foreground: 222.2 47.4% 11.2%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 100% 50%; - --destructive-foreground: 210 40% 98%; - --ring: 215 20.2% 65.1%; - --radius: 0.5rem; + --tc-background: 0 0% 100%; + --tc-foreground: 222.2 47.4% 11.2%; + --tc-muted: 210 40% 96.1%; + --tc-muted-foreground: 215.4 16.3% 46.9%; + --tc-popover: 0 0% 100%; + --tc-popover-foreground: 222.2 47.4% 11.2%; + --tc-border: 214.3 31.8% 91.4%; + --tc-input: 214.3 31.8% 91.4%; + --tc-card: 0 0% 100%; + --tc-card-foreground: 222.2 47.4% 11.2%; + --tc-primary: 222.2 47.4% 11.2%; + --tc-primary-foreground: 210 40% 98%; + --tc-secondary: 210 40% 96.1%; + --tc-secondary-foreground: 222.2 47.4% 11.2%; + --tc-accent: 210 40% 96.1%; + --tc-accent-foreground: 222.2 47.4% 11.2%; + --tc-destructive: 0 100% 50%; + --tc-destructive-foreground: 210 40% 98%; + --tc-ring: 215 20.2% 65.1%; + --tc-radius: 0.5rem; } .dark { - --background: 224 71% 4%; - --foreground: 213 31% 91%; - --muted: 223 47% 11%; - --muted-foreground: 215.4 16.3% 56.9%; - --accent: 216 34% 17%; - --accent-foreground: 210 40% 98%; - --popover: 224 71% 4%; - --popover-foreground: 215 20.2% 65.1%; - --border: 216 34% 17%; - --input: 216 34% 17%; - --card: 224 71% 4%; - --card-foreground: 213 31% 91%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 1.2%; - --secondary: 222.2 47.4% 11.2%; - --secondary-foreground: 210 40% 98%; - --destructive: 0 63% 31%; - --destructive-foreground: 210 40% 98%; - --ring: 216 34% 17%; - } -} - -/* @layer base { - * { - @apply border-border; - } - body { - @apply font-sans antialiased bg-background text-foreground; - } -} - - -@layer base { - :root { - font-family: Inter, system-ui, sans-serif; - } -} */ - -@layer components { - .btn { - @apply inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50; - } - - .btn-primary { - @apply bg-primary-600 text-white hover:bg-primary-700; - } - - .btn-secondary { - @apply bg-bg-200 text-text-700 hover:bg-bg-300; - } - - .btn-outline { - @apply border border-bg-300 bg-transparent text-text-700 hover:bg-bg-100; - } -} - -@layer utilities { - .text-gradient { - @apply bg-gradient-to-r from-primary-600 to-primary-400 bg-clip-text text-transparent; + --tc-background: 224 71% 4%; + --tc-foreground: 213 31% 91%; + --tc-muted: 223 47% 11%; + --tc-muted-foreground: 215.4 16.3% 56.9%; + --tc-accent: 216 34% 17%; + --tc-accent-foreground: 210 40% 98%; + --tc-popover: 224 71% 4%; + --tc-popover-foreground: 215 20.2% 65.1%; + --tc-border: 216 34% 17%; + --tc-input: 216 34% 17%; + --tc-card: 224 71% 4%; + --tc-card-foreground: 213 31% 91%; + --tc-primary: 210 40% 98%; + --tc-primary-foreground: 222.2 47.4% 1.2%; + --tc-secondary: 222.2 47.4% 11.2%; + --tc-secondary-foreground: 210 40% 98%; + --tc-destructive: 0 63% 31%; + --tc-destructive-foreground: 210 40% 98%; + --tc-ring: 216 34% 17%; } } diff --git a/extensions/chrome/src/types/index.ts b/extensions/chrome/src/types/index.ts deleted file mode 100644 index d8e8c1a..0000000 --- a/extensions/chrome/src/types/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -export interface Selectors { - pre: string - thinkingProcess: string - messageContainer: string - thinkingLabel: string - codeContainer: string - code: string -} - -export interface Timings { - retryDelay: number - mutationDelay: number - checkInterval: number - copyFeedback: number - maxRetries: number -} - -export interface Icons { - arrow: string - tick: string - copy: string -} - -export interface Styles { - animation: string - buttonClass: string - labelClass: string -} diff --git a/extensions/chrome/src/utils/dom-utils.ts b/extensions/chrome/src/utils/dom-utils.ts deleted file mode 100644 index 8d573e7..0000000 --- a/extensions/chrome/src/utils/dom-utils.ts +++ /dev/null @@ -1,44 +0,0 @@ -export const waitForElement = ( - selector: string, - timeout = 5000 -): Promise => { - return new Promise((resolve) => { - if (document.querySelector(selector)) { - return resolve(document.querySelector(selector)) - } - - const observer = new MutationObserver(() => { - if (document.querySelector(selector)) { - observer.disconnect() - resolve(document.querySelector(selector)) - } - }) - - observer.observe(document.body, { - childList: true, - subtree: true, - }) - - setTimeout(() => { - observer.disconnect() - resolve(null) - }, timeout) - }) -} - -export const findElement = async ( - selectors: string[] -): Promise => { - for (const selector of selectors) { - const element = await waitForElement(selector) - if (element) { - console.log("[Thinking Claude] Found element using selector:", selector) - return element - } - } - console.log( - "[Thinking Claude] No matching element found for selectors:", - selectors - ) - return null -} diff --git a/extensions/chrome/src/utils/url-utils.ts b/extensions/chrome/src/utils/url-utils.ts new file mode 100644 index 0000000..4f06dbc --- /dev/null +++ b/extensions/chrome/src/utils/url-utils.ts @@ -0,0 +1,7 @@ +export const isChatPage = (url: string): boolean => { + return url.startsWith("https://claude.ai/chat/") +} + +export const shouldInitialize = (url: string): boolean => { + return isChatPage(url) +} diff --git a/extensions/chrome/tailwind.config.cjs b/extensions/chrome/tailwind.config.cjs index 53bc121..996fdd6 100644 --- a/extensions/chrome/tailwind.config.cjs +++ b/extensions/chrome/tailwind.config.cjs @@ -1,50 +1,114 @@ +/**Know Issue for now: + * Our own Tailwind CSS is not being bundled into the build. + * TODO: Figure out why and fix this later. + */ +const colors = require("tailwindcss/colors") + /** @type {import('tailwindcss').Config} */ module.exports = { - darkMode: ['class'], - content: ['./src/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}', './src/content/**/*.{ts,tsx}'], + prefix: "tc-", + important: true, + content: [ + "./src/**/*.{ts,tsx}", + "./src/components/**/*.{ts,tsx}", + "./src/content/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx,html,js,jsx}", + "./public/**/*.html", + ], theme: { extend: { colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', + border: "hsl(var(--tc-border))", + input: "hsl(var(--tc-input))", + ring: "hsl(var(--tc-ring))", + background: "hsl(var(--tc-background))", + foreground: "hsl(var(--tc-foreground))", primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', + DEFAULT: "hsl(var(--tc-primary))", + foreground: "hsl(var(--tc-primary-foreground))", }, secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', + DEFAULT: "hsl(var(--tc-secondary))", + foreground: "hsl(var(--tc-secondary-foreground))", }, destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', + DEFAULT: "hsl(var(--tc-destructive))", + foreground: "hsl(var(--tc-destructive-foreground))", }, muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', + DEFAULT: "hsl(var(--tc-muted))", + foreground: "hsl(var(--tc-muted-foreground))", }, accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', + DEFAULT: "hsl(var(--tc-accent))", + foreground: "hsl(var(--tc-accent-foreground))", }, popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', + DEFAULT: "hsl(var(--tc-popover))", + foreground: "hsl(var(--tc-popover-foreground))", }, card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', + DEFAULT: "hsl(var(--tc-card))", + foreground: "hsl(var(--tc-card-foreground))", }, + // Include Tailwind's default colors + slate: colors.slate, + gray: colors.gray, + zinc: colors.zinc, + neutral: colors.neutral, + stone: colors.stone, + red: colors.red, + orange: colors.orange, + amber: colors.amber, + yellow: colors.yellow, + lime: colors.lime, + green: colors.green, + emerald: colors.emerald, + teal: colors.teal, + cyan: colors.cyan, + sky: colors.sky, + blue: colors.blue, + indigo: colors.indigo, + violet: colors.violet, + purple: colors.purple, + fuchsia: colors.fuchsia, + pink: colors.pink, + rose: colors.rose, }, - borderRadius: { - lg: `var(--radius)`, - md: `calc(var(--radius) - 2px)`, - sm: 'calc(var(--radius) - 4px)', + // we can inherit the original border radius form claude here + // borderRadius: { + // lg: "`var(--radius)`", + // md: "`calc(var(--radius) - 2px)`", + // sm: "calc(var(--radius) - 4px)", + // }, + keyframes: { + "accordion-down": { + from: { + height: "0", + }, + to: { + height: "var(--radix-accordion-content-height)", + }, + }, + "accordion-up": { + from: { + height: "var(--radix-accordion-content-height)", + }, + to: { + height: "0", + }, + }, + // shimmer: { + // "0%": { backgroundPosition: "200% 50%" }, + // "100%": { backgroundPosition: "-200% 50%" }, + // }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + // shimmer: "shimmer 3s linear infinite", }, }, }, - plugins: [require('tailwindcss-animate')], -}; + plugins: [require("tailwindcss-animate")], +} diff --git a/extensions/chrome/test.txt b/extensions/chrome/test.txt index 45f8b44..7001271 100644 --- a/extensions/chrome/test.txt +++ b/extensions/chrome/test.txt @@ -1 +1,2 @@ -Test file to verify github actions auto bumping +Version Bumping Test File +v3.1.0 \ No newline at end of file diff --git a/extensions/chrome/webpack/webpack.common.js b/extensions/chrome/webpack/webpack.common.js index a285e3b..1cfcc23 100644 --- a/extensions/chrome/webpack/webpack.common.js +++ b/extensions/chrome/webpack/webpack.common.js @@ -10,7 +10,7 @@ export default { entry: { // popup: path.resolve(__dirname, '..', 'src', 'popup', 'index.tsx'), //popup is not being developed yet // background: path.resolve(__dirname, '..', 'src', 'background', 'index.ts'), //background is not being developed yet - content: path.resolve(__dirname, "..", "src", "content", "index.tsx"), + content: path.resolve(__dirname, "..", "src", "content", "index.ts"), }, module: { rules: [ @@ -29,7 +29,23 @@ export default { }, { test: /\.css$/, - use: ["style-loader", "css-loader", "postcss-loader"], + use: [ + "style-loader", + { + loader: "css-loader", + options: { + importLoaders: 1, + }, + }, + { + loader: "postcss-loader", + options: { + postcssOptions: { + config: path.resolve(__dirname, "..", "postcss.config.cjs"), + }, + }, + }, + ], }, ], },