mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat(web): redesign Slack page Featured Workflow section with YouTube… (#10880)
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com> Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
parent
2305888746
commit
d87abe8efc
3 changed files with 633 additions and 68 deletions
|
|
@ -5,7 +5,6 @@ import {
|
|||
GitBranch,
|
||||
GraduationCap,
|
||||
Link2,
|
||||
LucideIcon,
|
||||
MessageSquare,
|
||||
Settings,
|
||||
Shield,
|
||||
|
|
@ -13,13 +12,15 @@ import {
|
|||
Users,
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { SlackThreadDemo } from "@/components/slack/slack-thread-demo"
|
||||
import { Button } from "@/components/ui"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
|
||||
const TITLE = "Roo Code for Slack"
|
||||
const DESCRIPTION =
|
||||
|
|
@ -69,7 +70,7 @@ export const metadata: Metadata = {
|
|||
// Invalidate cache when a request comes in, at most once every hour.
|
||||
export const revalidate = 3600
|
||||
|
||||
interface ValueProp {
|
||||
type ValueProp = {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
|
|
@ -112,11 +113,10 @@ const VALUE_PROPS: ValueProp[] = [
|
|||
},
|
||||
]
|
||||
|
||||
interface WorkflowStep {
|
||||
type WorkflowStep = {
|
||||
step: number
|
||||
title: string
|
||||
description: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
const WORKFLOW_STEPS: WorkflowStep[] = [
|
||||
|
|
@ -124,7 +124,6 @@ const WORKFLOW_STEPS: WorkflowStep[] = [
|
|||
step: 1,
|
||||
title: "Turn the discussion into a plan",
|
||||
description: "Your team discusses a feature. When it gets complex, summon the Planner agent.",
|
||||
code: "@Roomote plan out a dark mode feature based on our discussion. Include the toggle, persistence, and system preference detection.",
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
|
|
@ -136,7 +135,6 @@ const WORKFLOW_STEPS: WorkflowStep[] = [
|
|||
step: 3,
|
||||
title: "Build the plan",
|
||||
description: "Once the plan looks good, hand it off to the Coder agent to implement.",
|
||||
code: "@Roomote implement this plan in the frontend-web repo.",
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
|
|
@ -145,7 +143,7 @@ const WORKFLOW_STEPS: WorkflowStep[] = [
|
|||
},
|
||||
]
|
||||
|
||||
interface OnboardingStep {
|
||||
type OnboardingStep = {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
|
|
@ -182,48 +180,54 @@ const ONBOARDING_STEPS: OnboardingStep[] = [
|
|||
},
|
||||
]
|
||||
|
||||
export default function SlackPage() {
|
||||
export default function SlackPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex pt-32 pb-20 items-center overflow-hidden">
|
||||
<AnimatedBackground />
|
||||
<div className="container relative flex flex-col items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center max-w-4xl mx-auto mb-12">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 text-sm font-medium mb-6">
|
||||
<Slack className="size-4" />
|
||||
Powered by Roo Code Cloud
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 items-center gap-10 lg:grid-cols-2 lg:gap-12">
|
||||
<div className="text-center lg:text-left">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 text-sm font-medium mb-6">
|
||||
<Slack className="size-4" />
|
||||
Powered by Roo Code Cloud
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-5xl lg:text-6xl">
|
||||
<span className="text-violet-500">@Roomote:</span> Your AI Team in Slack
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto lg:mx-0">
|
||||
Mention @Roomote in any channel to explain code, plan features, or ship a PR, all
|
||||
without leaving the conversation.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
|
||||
<Button
|
||||
size="xl"
|
||||
className="bg-violet-600 hover:bg-violet-700 text-white transition-all duration-300 shadow-lg hover:shadow-violet-500/25"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Get Started
|
||||
<ArrowRight className="ml-2 size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" size="xl" className="backdrop-blur-sm" asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.SLACK_DOCS}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Read the Docs
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-5xl lg:text-6xl">
|
||||
<span className="text-violet-500">@Roomote:</span> Your AI Team in Slack
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
|
||||
Mention @Roomote in any channel to explain code, plan features, or ship a PR, all without
|
||||
leaving the conversation.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
size="xl"
|
||||
className="bg-violet-600 hover:bg-violet-700 text-white transition-all duration-300 shadow-lg hover:shadow-violet-500/25"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Get Started
|
||||
<ArrowRight className="ml-2 size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" size="xl" className="backdrop-blur-sm" asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.SLACK_DOCS}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Read the Docs
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<div className="flex justify-center lg:justify-end">
|
||||
<SlackThreadDemo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -264,13 +268,13 @@ export default function SlackPage() {
|
|||
</section>
|
||||
|
||||
{/* Featured Workflow Section */}
|
||||
<section className="relative overflow-hidden border-t border-border py-32">
|
||||
<section className="relative overflow-hidden border-t border-border py-24 lg:py-32">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
|
||||
<div className="mx-auto mb-12 max-w-5xl text-center">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm font-medium mb-6">
|
||||
<Zap className="size-4" />
|
||||
Featured Workflow
|
||||
|
|
@ -283,29 +287,42 @@ export default function SlackPage() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto md:max-w-[1000px]">
|
||||
{/* Workflow Steps */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
|
||||
{WORKFLOW_STEPS.map((step) => (
|
||||
<div
|
||||
key={step.step}
|
||||
className="relative border border-border rounded-2xl bg-background p-8 transition-all duration-300 hover:shadow-lg">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="bg-blue-100 dark:bg-blue-900/30 w-10 h-10 rounded-full flex items-center justify-center text-blue-700 dark:text-blue-300 font-bold">
|
||||
{step.step}
|
||||
<div className="relative mx-auto max-w-6xl">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-10 items-center">
|
||||
{/* YouTube Video Embed */}
|
||||
<div className="lg:col-span-3 overflow-hidden rounded-2xl border border-border bg-background shadow-lg">
|
||||
<iframe
|
||||
className="aspect-video w-full"
|
||||
src="https://www.youtube-nocookie.com/embed/dJM_8HHGe1E?rel=0"
|
||||
title="Roo Code Slack Integration Demo"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Workflow Steps */}
|
||||
<div className="lg:col-span-2 space-y-3">
|
||||
{WORKFLOW_STEPS.map((step) => (
|
||||
<div
|
||||
key={step.step}
|
||||
className="relative border border-border rounded-xl bg-background p-4 transition-all duration-300 hover:shadow-md hover:border-blue-500/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-blue-100 dark:bg-blue-900/30 w-7 h-7 rounded-full flex items-center justify-center text-blue-700 dark:text-blue-300 font-bold text-xs shrink-0 mt-0.5">
|
||||
{step.step}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-base font-semibold text-foreground mb-0.5">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-sm leading-snug text-muted-foreground">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-foreground">{step.title}</h3>
|
||||
</div>
|
||||
<p className="leading-relaxed font-light text-muted-foreground mb-4">
|
||||
{step.description}
|
||||
</p>
|
||||
{step.code && (
|
||||
<div className="bg-muted/50 rounded-lg p-4 font-mono text-sm text-foreground/80 border border-border/50">
|
||||
{step.code}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
548
apps/web-roo-code/src/components/slack/slack-thread-demo.tsx
Normal file
548
apps/web-roo-code/src/components/slack/slack-thread-demo.tsx
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
"use client"
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { CheckCircle2, Paperclip } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type SlackMessage = {
|
||||
id: string
|
||||
author: string
|
||||
timeLabel: string
|
||||
body: ReactNode
|
||||
avatarText: string
|
||||
avatarClassName: string
|
||||
kind: "human" | "bot"
|
||||
}
|
||||
|
||||
function usePrefersReducedMotion(): boolean {
|
||||
const [reduced, setReduced] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia("(prefers-reduced-motion: reduce)")
|
||||
const onChange = () => setReduced(media.matches)
|
||||
onChange()
|
||||
|
||||
if (typeof media.addEventListener === "function") {
|
||||
media.addEventListener("change", onChange)
|
||||
return () => media.removeEventListener("change", onChange)
|
||||
}
|
||||
|
||||
media.addListener?.(onChange)
|
||||
return () => media.removeListener?.(onChange)
|
||||
}, [])
|
||||
|
||||
return reduced
|
||||
}
|
||||
|
||||
type TypingDotsProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
function TypingDots({ className }: TypingDotsProps): JSX.Element {
|
||||
return (
|
||||
<span className={cn("inline-flex items-center gap-1", className)} aria-hidden="true">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:0ms]" />
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:180ms]" />
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:360ms]" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
type FakeLinkProps = {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
function FakeLink({ children, className }: FakeLinkProps): JSX.Element {
|
||||
return (
|
||||
<span className={cn("text-violet-300 underline underline-offset-2", "cursor-default", className)}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
type SlackMessageRowProps = {
|
||||
message: SlackMessage
|
||||
isNew: boolean
|
||||
reduceMotion: boolean
|
||||
}
|
||||
|
||||
function SlackMessageRow({ message, isNew, reduceMotion }: SlackMessageRowProps): JSX.Element {
|
||||
let animation = ""
|
||||
if (!reduceMotion && isNew) {
|
||||
animation = "animate-in fade-in slide-in-from-bottom-2 duration-500"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex gap-3", animation)}>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-xs font-semibold",
|
||||
message.avatarClassName,
|
||||
)}>
|
||||
{message.avatarText}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||
<span className="text-[13px] font-semibold text-[#F8F8F9]">{message.author}</span>
|
||||
<span className="text-[11px] text-[#8B8D91]">{message.timeLabel}</span>
|
||||
{message.kind === "bot" && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-violet-500/20 px-2 py-0.5 text-[10px] font-medium text-violet-200">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
App
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 text-[13px] leading-relaxed text-[#D1D2D3]">{message.body}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export type SlackThreadDemoProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function SlackThreadDemo({ className }: SlackThreadDemoProps): JSX.Element {
|
||||
const reduceMotion = usePrefersReducedMotion()
|
||||
const [stepIndex, setStepIndex] = useState(0)
|
||||
const scrollViewportRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const messages: SlackMessage[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "m1",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 2:56 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<span>We need to add a page to our Marketing site that highlights using Roo Code from Slack.</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m2",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 2:58 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
The documentation for using Roo Code from Slack is here:{" "}
|
||||
<FakeLink className="hover:text-violet-200">
|
||||
https://docs.roocode.com/roo-code-cloud/slack-integration
|
||||
</FakeLink>
|
||||
</div>
|
||||
<div className="text-[#B8BBC0]">Here are some pages from our site we can use for guidance:</div>
|
||||
<ol className="list-decimal pl-5 text-[#D1D2D3]">
|
||||
<li>
|
||||
<FakeLink className="hover:text-violet-200">https://roocode.com</FakeLink>
|
||||
</li>
|
||||
<li>
|
||||
<FakeLink className="hover:text-violet-200">https://roocode.com/extension</FakeLink>
|
||||
</li>
|
||||
<li>
|
||||
<FakeLink className="hover:text-violet-200">https://roocode.com/cloud</FakeLink>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m3",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 3:08 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<div className="space-y-3">
|
||||
<div>This is the start of a wireframe I have in mind for this page</div>
|
||||
<div className="w-full max-w-[420px] rounded-lg border border-white/10 bg-black/20 p-3">
|
||||
<div className="flex items-center gap-2 text-[12px] text-[#B8BBC0]">
|
||||
<Paperclip className="h-4 w-4" />
|
||||
IMG_9721.heic
|
||||
</div>
|
||||
<div className="mt-3 h-24 w-full rounded-md bg-gradient-to-br from-white/10 via-white/5 to-white/0" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m4",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 3:09 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<div className="space-x-2">
|
||||
<FakeLink className="no-underline hover:text-violet-200">@Roomote</FakeLink>
|
||||
<span>let's create the plan to deliver this</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m5",
|
||||
author: "Roomote (Roo Code)",
|
||||
timeLabel: "Monday at 3:09 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2 text-[#D1D2D3]">
|
||||
Calling <span className="font-semibold text-[#F8F8F9]">Planneroo</span> to get started on
|
||||
your task on{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
RooCodeInc/Roo-Code
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center rounded-md border border-white/10 bg-transparent px-2 py-1 text-[12px] font-medium text-[#D1D2D3] hover:bg-white/5">
|
||||
Cancel ✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m6",
|
||||
author: "Roomote (Roo Code)",
|
||||
timeLabel: "Monday at 3:10 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-x-2">
|
||||
<span>Cool, I'll knock this out real quick.</span>
|
||||
<FakeLink className="hover:text-violet-200">Follow along</FakeLink>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m7",
|
||||
author: "Roomote (Roo Code)",
|
||||
timeLabel: "Monday at 3:12 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-y-2">
|
||||
<div className="font-semibold text-[#F8F8F9]">Todo List:</div>
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
"Analyze existing page structures and component patterns",
|
||||
"Review marketing content requirements and wireframe details",
|
||||
"Create detailed component architecture plan",
|
||||
"Design page structure and section breakdown",
|
||||
"Plan navigation updates and integration points",
|
||||
"Test the page and verify all sections work",
|
||||
].map((item) => (
|
||||
<li key={item} className="text-[#D1D2D3]">
|
||||
<span className="mr-2">•</span>
|
||||
<span className="line-through opacity-80">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="text-[12px] text-[#8B8D91]">(edited)</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m8",
|
||||
author: "Roomote (Roo Code)",
|
||||
timeLabel: "Monday at 3:16 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-y-3">
|
||||
<p>
|
||||
I've created a comprehensive implementation plan for the Roo Code Slack integration
|
||||
marketing page at{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
plans/slack-marketing-page-plan.md
|
||||
</code>
|
||||
.
|
||||
</p>
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<div className="text-[12px] font-semibold text-[#F8F8F9]">Plan Overview</div>
|
||||
<ul className="mt-2 space-y-1 text-[#D1D2D3]">
|
||||
<li>
|
||||
<span className="mr-2">•</span>Hero + dual CTAs
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>Value props grid
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>“Thread to Shipped Feature” workflow
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>Onboarding steps + CTA
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<span className="text-[#B8BBC0]">Full document:</span>
|
||||
<FakeLink className="hover:text-violet-200">View artifact</FakeLink>
|
||||
</div>
|
||||
<div className="text-[12px] italic text-[#8B8D91]">
|
||||
Want to follow up? Just @-mention me in your response.
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m9",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 3:17 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<div className="space-x-2">
|
||||
<FakeLink className="no-underline hover:text-violet-200">@Roomote (Roo Code)</FakeLink>
|
||||
<span>this looks great, let's use Coderoo to build this</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m10",
|
||||
author: "Roomote (Roo Code)",
|
||||
timeLabel: "Monday at 3:23 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-y-3">
|
||||
<p>
|
||||
I've built the Roo Code Slack integration marketing page. Here's what was
|
||||
implemented:
|
||||
</p>
|
||||
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<div className="text-[12px] font-semibold text-[#F8F8F9]">Files</div>
|
||||
<ul className="mt-2 space-y-1 text-[#D1D2D3]">
|
||||
<li>
|
||||
<span className="mr-2">•</span>
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
apps/web-roo-code/src/app/slack/page.tsx
|
||||
</code>{" "}
|
||||
— Slack marketing page
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
apps/web-roo-code/src/lib/constants.ts
|
||||
</code>{" "}
|
||||
— added{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
SLACK_DOCS
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
apps/web-roo-code/src/components/chromes/nav-bar.tsx
|
||||
</code>{" "}
|
||||
— added Slack to Product dropdown
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<div className="text-[12px] font-semibold text-[#F8F8F9]">Pull Request</div>
|
||||
<div className="mt-2">
|
||||
<span className="font-semibold text-[#F8F8F9]">PR #10853</span>:{" "}
|
||||
<FakeLink className="hover:text-violet-200">
|
||||
https://github.com/RooCodeInc/Roo-Code/pull/10853
|
||||
</FakeLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[#B8BBC0]">
|
||||
The page is accessible at{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">/slack</code>{" "}
|
||||
and includes navigation links in desktop and mobile.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
type DemoPhase =
|
||||
| { kind: "show"; messageIndex: number }
|
||||
| { kind: "typing"; messageIndex: number }
|
||||
| { kind: "reset" }
|
||||
|
||||
const phases: DemoPhase[] = useMemo(() => {
|
||||
const next: DemoPhase[] = []
|
||||
if (messages.length === 0) return [{ kind: "reset" }]
|
||||
|
||||
next.push({ kind: "typing", messageIndex: 0 })
|
||||
next.push({ kind: "show", messageIndex: 0 })
|
||||
for (let messageIndex = 1; messageIndex < messages.length; messageIndex += 1) {
|
||||
next.push({ kind: "typing", messageIndex })
|
||||
next.push({ kind: "show", messageIndex })
|
||||
}
|
||||
next.push({ kind: "reset" })
|
||||
return next
|
||||
}, [messages])
|
||||
|
||||
const lastShowPhaseIndex = useMemo(() => {
|
||||
let lastIndex = -1
|
||||
for (let idx = 0; idx < phases.length; idx += 1) {
|
||||
if (phases[idx]?.kind === "show") lastIndex = idx
|
||||
}
|
||||
return lastIndex
|
||||
}, [phases])
|
||||
|
||||
useEffect(() => {
|
||||
if (reduceMotion) {
|
||||
setStepIndex(lastShowPhaseIndex >= 0 ? lastShowPhaseIndex : 0)
|
||||
return
|
||||
}
|
||||
|
||||
const active = phases[stepIndex] ?? phases.at(0)
|
||||
const isLastMessageShow = active?.kind === "show" && stepIndex === lastShowPhaseIndex
|
||||
const durationMs = (() => {
|
||||
const base = 2200
|
||||
if (active?.kind === "reset") return 500
|
||||
if (active?.kind === "typing") return 900
|
||||
return isLastMessageShow ? base * 2 : base
|
||||
})()
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
setStepIndex((prev) => (prev + 1) % phases.length)
|
||||
}, durationMs)
|
||||
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [lastShowPhaseIndex, phases, reduceMotion, stepIndex])
|
||||
|
||||
const activePhase = phases[stepIndex] ?? phases.at(0) ?? { kind: "reset" }
|
||||
|
||||
function getVisibleCount(phase: DemoPhase): number {
|
||||
if (phase.kind === "reset") return 0
|
||||
if (phase.kind === "typing") return phase.messageIndex
|
||||
return phase.messageIndex + 1
|
||||
}
|
||||
|
||||
const visibleCount = getVisibleCount(activePhase)
|
||||
const visibleMessages = messages.slice(0, visibleCount)
|
||||
const typingTarget = activePhase.kind === "typing" ? messages[activePhase.messageIndex] : undefined
|
||||
|
||||
useEffect(() => {
|
||||
const viewport = scrollViewportRef.current
|
||||
if (!viewport) return
|
||||
|
||||
if (activePhase.kind === "reset" || visibleCount <= 1) {
|
||||
viewport.scrollTo({ top: 0, behavior: "auto" })
|
||||
return
|
||||
}
|
||||
|
||||
viewport.scrollTo({
|
||||
top: viewport.scrollHeight,
|
||||
behavior: reduceMotion ? "auto" : "smooth",
|
||||
})
|
||||
}, [activePhase.kind, reduceMotion, visibleCount])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("w-full max-w-[620px] h-[520px] sm:h-[560px]", className)}
|
||||
role="img"
|
||||
aria-label="Animated Slack thread showing Roo Code responding as @Roomote">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="relative flex h-full flex-col overflow-hidden rounded-2xl border border-white/10 bg-[#1A1D21] shadow-2xl shadow-black/30">
|
||||
<div className="flex items-center justify-between border-b border-white/10 px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#F24A4A]" />
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#F2C94C]" />
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#27AE60]" />
|
||||
<div className="ml-3 text-sm font-semibold text-[#F8F8F9]">Thread</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[11px] text-[#8B8D91]">
|
||||
<span className="h-2 w-2 rounded-full bg-[#27AE60]" />
|
||||
Live demo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={scrollViewportRef}
|
||||
className="flex-1 overflow-y-auto px-4 py-5 [scrollbar-width:thin] [scrollbar-color:rgba(255,255,255,0.18)_transparent]">
|
||||
<div
|
||||
className={cn(
|
||||
"space-y-5 transition-opacity duration-300 will-change-opacity",
|
||||
activePhase.kind === "reset" ? "opacity-0" : "opacity-100",
|
||||
)}>
|
||||
{visibleMessages.map((message) => (
|
||||
<SlackMessageRow
|
||||
key={message.id}
|
||||
message={message}
|
||||
reduceMotion={reduceMotion}
|
||||
isNew={
|
||||
activePhase.kind === "show" && messages[activePhase.messageIndex]?.id === message.id
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{typingTarget && (
|
||||
<div className={cn(reduceMotion ? "" : "animate-in fade-in duration-300", "flex gap-3")}>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-xs font-semibold",
|
||||
typingTarget.avatarClassName,
|
||||
)}>
|
||||
{typingTarget.avatarText}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-baseline gap-x-2">
|
||||
<span className="text-[13px] font-semibold text-[#F8F8F9]">
|
||||
{typingTarget.author}
|
||||
</span>
|
||||
{typingTarget.kind === "bot" && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-violet-500/20 px-2 py-0.5 text-[10px] font-medium text-violet-200">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
App
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[11px] text-[#8B8D91]">typing…</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<TypingDots />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-white/10 px-4 py-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{messages.map((message, idx) => (
|
||||
<span
|
||||
key={message.id}
|
||||
className={cn(
|
||||
"h-1.5 w-5 rounded-full transition-colors duration-300",
|
||||
Math.max(0, visibleCount - 1) === idx ? "bg-violet-300" : "bg-white/10",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
|
|||
|
||||
afterAll(async () => {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
}, 60_000) // 60 second timeout for Windows cleanup
|
||||
|
||||
describe(`${klass.name}#getDiff`, () => {
|
||||
it("returns the correct diff between commits", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue