Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/_components/blog/BlogList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function BlogList({ posts }: { posts: Item[] }) {

if (filteredPosts.length === 0) {
return (
<p className="text-base text-[#002424] mt-4 text-center">No results found.</p>
<p className="text-base text-[#002424] dark:text-zinc-200 mt-4 text-center">No results found.</p>
)
}

Expand Down
68 changes: 42 additions & 26 deletions app/_components/common/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,23 @@ import { Maximize2, Minimize2, Pause, Play, Volume2, VolumeX } from 'lucide-reac
import dynamic from 'next/dynamic'
import { ReactPlayerProps } from 'react-player'

type ControlOption = "muted" | "full-screen" | "play" | "progress" | "watch";

const ReactPlayer = dynamic(() => import('react-player'), { ssr: false })

interface PlayerProps extends ReactPlayerProps {
aspect?: "square" | "video"
interface PlayerProps extends Omit<ReactPlayerProps, "controls"> {
aspect?: "square" | "video";
controls?: ControlOption[] | boolean;
}

export default function Player({ aspect = "video", playing: initialPlaying, muted: initialMuted, ...props }: PlayerProps) {
export default function Player({ aspect = "video", controls = true, playing: initialPlaying, muted: initialMuted, ...props }: PlayerProps) {
const { containerRef, playerRef, playing, muted, played, duration, isFullScreen, handleProgress, handleDuration, handlePlayPause, handleMuted, handleSeekChange, handleSeekMouseDown, handleSeekMouseUp, handleFullscreen } = usePlayer({ playing: initialPlaying, muted: initialMuted });

const buttonStyle = cx("w-11 h-11 flex items-center justify-center rounded-full bg-[#67675780] outline-[0.8px] outline-[#FFFFFF] -outline-offset-[3px] hover:outline-[#06474C] text-[#FFFFFF] duration-200 transition-all hover:bg-[#FFFFFF] hover:text-[#06474C] cursor-pointer")

const showControl = (name: ControlOption) =>
controls === true || (Array.isArray(controls) && controls.includes(name));

return (
<div
ref={containerRef}
Expand All @@ -41,37 +47,47 @@ export default function Player({ aspect = "video", playing: initialPlaying, mute
<div className="absolute bottom-0 left-0 flex flex-col gap-3 bg-linear-to-b px-6 pb-6 py-2 from-transparent to-[#6E6E68]/60 w-full">
<div className="flex gap-2 justify-between">
<div className="flex gap-2">
<button onClick={handlePlayPause} className={buttonStyle}>
{playing ? <Pause strokeWidth={1} size={16} /> : <Play strokeWidth={1} size={16} />}
</button>
<span className='h-11 w-[117px] flex items-center justify-center rounded-full bg-[#67675780] text-[#FFFFFF] text-lg'>
{secondsToMinutes((played * duration).toFixed(1))} / {secondsToMinutes(duration.toFixed(1))}
</span>
{showControl("play") && (
<button onClick={handlePlayPause} className={buttonStyle}>
{playing ? <Pause strokeWidth={1} size={16} /> : <Play strokeWidth={1} size={16} />}
</button>
)}

{showControl("muted") && (
<span className='h-11 w-[117px] flex items-center justify-center rounded-full bg-[#67675780] text-[#FFFFFF] text-lg'>
{secondsToMinutes((played * duration).toFixed(1))} / {secondsToMinutes(duration.toFixed(1))}
</span>
)}
</div>

<div className="flex gap-2">
<button onClick={handleMuted} className={buttonStyle}>
{muted ? <VolumeX strokeWidth={1} size={16} /> : <Volume2 strokeWidth={1} size={16} />}
</button>
{showControl("muted") && (
<button onClick={handleMuted} className={buttonStyle}>
{muted ? <VolumeX strokeWidth={1} size={16} /> : <Volume2 strokeWidth={1} size={16} />}
</button>
)}

<button onClick={handleFullscreen} className={buttonStyle}>
{isFullScreen ? <Minimize2 strokeWidth={1} size={16} /> : <Maximize2 strokeWidth={1} size={16} />}
</button>
{showControl("full-screen") && (
<button onClick={handleFullscreen} className={buttonStyle}>
{isFullScreen ? <Minimize2 strokeWidth={1} size={16} /> : <Maximize2 strokeWidth={1} size={16} />}
</button>
)}
</div>
</div>

<input
type="range"
min={0}
max={1}
step="any"
value={played}
onMouseDown={handleSeekMouseDown}
onChange={handleSeekChange}
onMouseUp={handleSeekMouseUp}
className='appearance-none overflow-hidden cursor-pointer rounded-full [&::-webkit-slider-runnable-track]:bg-[#D4D4C9B2] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:shadow-[-1444px_0_0_1440px_#67675780] [&::-webkit-slider-thumb]:h-2 [&::-webkit-slider-thumb]:w-2 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[#67675780]'
/>
{showControl("progress") && (
<input
type="range"
min={0}
max={1}
step="any"
value={played}
onMouseDown={handleSeekMouseDown}
onChange={handleSeekChange}
onMouseUp={handleSeekMouseUp}
className='appearance-none overflow-hidden cursor-pointer rounded-full [&::-webkit-slider-runnable-track]:bg-[#D4D4C9B2] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:shadow-[-1444px_0_0_1440px_#67675780] [&::-webkit-slider-thumb]:h-2 [&::-webkit-slider-thumb]:w-2 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[#67675780]'
/>
)}
</div>
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion app/_components/home/Client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function HomeClient({ posts }: { posts: Item[] }) {
<Welcome />
<HowItWorks />
<Service />
<div ref={darkSectionRef}>
<div ref={darkSectionRef} className="w-full">
<OpenSource />
<Roadmap />
</div>
Expand Down
25 changes: 23 additions & 2 deletions app/_components/home/HowItWorks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { Button } from "../common/button";
import Image from "next/image";
import { openToast } from "../common/toast";
import { addBasePath } from "@/app/_lib/add-base-path";
import { useScreenSize } from "@/app/_hooks/useScreenSize";

export default function HowItWorks() {
const { isMobile } = useScreenSize();

return (
<section className="flex w-full max-w-[1440px] flex-col gap-5 px-4 py-24 text-[#002424] md:px-8 md:py-40">
<p className="text-base">Coming soon</p>
Expand All @@ -21,15 +24,33 @@ export default function HowItWorks() {
<Button variant="secondary" className="w-fit" onClick={openToast}>
Agent OS <ArrowRightIcon strokeWidth={1.5} size={20} />
</Button>
<div className="relative aspect-72/31 min-h-[320px] w-full cursor-pointer overflow-hidden rounded-3xl">

{isMobile ? <div className="relative aspect-72/31 min-h-[320px] w-full cursor-pointer overflow-hidden rounded-3xl">
<Image
src={addBasePath("/images/main_mobile.png")}
alt="thumbnail"
fill
objectFit="cover"
className="scale-105 transition-all duration-750 hover:scale-100"
/>
</div> : <div className="relative aspect-72/31 min-h-[320px] w-full cursor-pointer overflow-hidden rounded-3xl">
<Image
src={addBasePath("/images/main.png")}
alt="thumbnail"
fill
objectFit="cover"
className="scale-105 transition-all duration-750 hover:scale-100"
/>
</div>
</div>}
{/* <div className="relative aspect-72/31 min-h-[320px] w-full cursor-pointer overflow-hidden rounded-3xl">
<Image
src={isMobile ? addBasePath("/images/main_mobile.png") : addBasePath("/images/main.png")}
alt="thumbnail"
fill
objectFit="cover"
className="scale-105 transition-all duration-750 hover:scale-100"
/>
</div> */}
</section>
);
}
2 changes: 1 addition & 1 deletion app/_components/home/Roadmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ROADMAP } from "@/app/_constants/roadmap";
export default function Roadmap() {
return (
<section className="flex w-full items-center justify-center bg-linear-to-b from-[#000000] to-[#002424] px-4 md:px-8 py-40 md:h-[960px] md:max-h-screen">
<div className="flex w-full max-w-[1440px] flex-col items-center justify-between gap-6 md:flex-row">
<div className="flex md:max-w-[1440px] flex-col items-center justify-between gap-6 md:flex-row">
<div className="flex flex-col gap-6">
<p className="text-base text-[#E6FDFC]">Roadmap</p>
<h2 className="font-oceanic text-[24px] leading-normal md:text-[40px] md:leading-[46px] whitespace-pre-line text-[#E6FDFC]">
Expand Down
10 changes: 6 additions & 4 deletions app/_components/home/Welcome.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useState } from "react";
import Player from "../common/Player";
import { PlayCircle } from "lucide-react";
import { useScreenSize } from "@/app/_hooks/useScreenSize";

export default function Welcome() {
const [viewPromotion, setViewPromotion] = useState(false);
const { isMobile } = useScreenSize();

return (
<section className="flex w-full flex-col gap-24 pt-40 px-4 md:px-40">
Expand All @@ -22,8 +24,8 @@ export default function Welcome() {
{viewPromotion ? (
<Player
url="https://studio-pro-fe.s3.ap-northeast-2.amazonaws.com/preview.mp4"
playing={true}
muted={true}
playing
muted
/>
) : (
<div className="relative group">
Expand All @@ -37,9 +39,9 @@ export default function Welcome() {
<source src="/videos/shortcut.mp4" type="video/mp4" />
<source src="/videos/shortcut.mp4" type="video/webm" />
</video>
<button className="absolute bottom-10 left-1/2 -translate-x-1/2 transition-all duration-300 cursor-pointer items-center gap-2 pl-5 pr-1 py-1 text-[#E6FDFC] group-hover:text-[#002424] flex rounded-full bg-[#002424] group-hover:bg-[#E6FDFC] text-lg" onClick={() => setViewPromotion(true)}>
<button className="absolute bottom-10 left-1/2 -translate-x-1/2 transition-all duration-300 cursor-pointer items-center gap-2 pl-4 md:pl-5 pr-1 py-1 text-[#E6FDFC] group-hover:text-[#002424] flex rounded-full bg-[#002424] group-hover:bg-[#E6FDFC] text-sm md:text-lg" onClick={() => setViewPromotion(true)}>
Watch Video
<PlayCircle size={48} strokeWidth={1} />
<PlayCircle size={isMobile ? 32 : 48} strokeWidth={1} />
</button>
</div>
)
Expand Down
44 changes: 44 additions & 0 deletions app/_hooks/useMediaQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useIsomorphicLayoutEffect } from "framer-motion";
import React from "react";

type UseMediaQueryOptions = {
defaultValue?: boolean;
initializeWithValue?: boolean;
};

export function useMediaQuery(
query: string,
{
defaultValue = false,
initializeWithValue = true,
}: UseMediaQueryOptions = {},
): boolean {
const isServer = typeof window === "undefined";

const getMatches = (query: string): boolean => {
if (isServer) return defaultValue;
return window.matchMedia(query).matches;
};

const [matches, setMatches] = React.useState<boolean>(() => {
if (initializeWithValue) {
return getMatches(query);
}
return defaultValue;
});

const handleChange = () => setMatches(getMatches(query));

useIsomorphicLayoutEffect(() => {
if (isServer) return;

const matchMedia = window.matchMedia(query);

handleChange();
matchMedia.addEventListener("change", handleChange);

return () => matchMedia.removeEventListener("change", handleChange);
}, [query]);

return matches;
}
16 changes: 16 additions & 0 deletions app/_hooks/useScreenSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useMediaQuery } from "./useMediaQuery";

export function useScreenSize() {
const sm = useMediaQuery(`(max-width: ${980 - 1}px)`);
const md = useMediaQuery(
`(min-width: ${980}px) and (max-width: ${1280 - 1}px)`,
);
const lg = useMediaQuery(`(min-width: ${1280}px)`);

return {
bp: sm ? "sm" : md ? "md" : "lg",
isMobile: sm,
isTablet: md,
isDesktop: lg,
};
}
24 changes: 21 additions & 3 deletions content/meet-our-new-member.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ But what if we could leverage AI technology to bring the conversational depth an

# Real Thoughts Emerge Through Conversations

<Player url="/contents/meet-our-new-member/3.mp4" aspect="square" />
<Player
url="/contents/meet-our-new-member/3.mp4"
aspect="square"
playing
muted
controls={["play"]}
/>

The Wrtn Labs team leveraged [**Agentica**](https://wrtnlabs.io/agentica/) to develop and test an interactive **Interview AI** that dynamically generates follow-up questions based on user responses. The approach involves clearly defining interview goals, the type of information desired, and the insights valuable to the product team. If a user provides a vague or brief response, the AI agent is designed to ask follow-up questions to gather more detailed and relevant information.

Expand All @@ -47,7 +53,13 @@ Yet, not every user initially provided detailed feedback. Those without specific

# Can AI Really Ask the Right Questions to User?

<Player url="/contents/meet-our-new-member/4.mov" aspect="square" />
<Player
url="/contents/meet-our-new-member/4.mov"
aspect="square"
playing
muted
controls={["play"]}
/>

To effectively capture interview objectives and desired information, while dynamically generating appropriate follow-up questions based on user responses, meticulous refinement of the prompts was necessary. We analyzed questions and response patterns from actual face-to-face interviews and reflected various examples of interview guides, primary questions, and potential follow-up questions in the prompts.

Expand All @@ -59,7 +71,13 @@ Of course, due to the nature of Large Language Models (LLMs), prompts are not re

# Organizing Insights Through AI Agents

<Player url="/contents/meet-our-new-member/5.mp4" aspect="square" />
<Player
url="/contents/meet-our-new-member/5.mp4"
aspect="square"
playing
muted
controls={["play"]}
/>

However, unlike structured surveys, conversational data gathered from many users proved challenging to organize clearly. Since insights were recorded conversationally, it was essential to structure them into charts or allow queries to focus on specific data points. Here, Agentica’s core functionality—the Connector—played a pivotal role. Within Agentica, any callable function can be turned into a Connector accessible by the AI agents whenever required. For example, when an Insight Extraction agent received a command such as "Show me user churn complaints," it could invoke a Connector to access records from the Interview agent, retrieve relevant data, and summarize the results clearly in text form. Thus, agents with different purposes and tools could communicate seamlessly to fulfill user requests.

Expand Down
Binary file added public/images/main_mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.