Interview
Interviews, gesetzt, damit man hört, wer spricht. Frage und Antwort mit Initialen am Rand, ein Porträt neben dem Gespräch, eine Runde, in der eine Stimme allein stehen kann, schnelle Fragen als Karten und die stärksten Zitate nacheinander.
Eine von 2 Components in Autoren und Interviews
Variants
Details
Was Interview mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Autoren und Interviews
- Bereich
- Content
- Variants
- 5
- Version
- 1.0.0 · seit 1.1.0
- Datei
- components/ui/interview.tsx
- Bringt mit
- utilskiticonrevealmotionsection
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 11 eigene Texte
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle InterviewProps aus components/ui/interview.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalInterviewVariantclassicportraitround-tablequick-firehighlights
eyebrowoptionalReact.ReactNodetitleoptionalReact.ReactNodedescriptionoptionalReact.ReactNodehostoptionalInterviewPersonWho asks
peopleoptionalInterviewPerson[]Who answers; round-table: everyone at the table
itemsoptionalInterviewItem[]mediaoptionalReact.ReactNodeportrait: the large picture
factsoptional{ label: string; value: string }[]portrait: facts, e.g. [{ label: 'Studio', value: 'Graz, since 2014' }]
quotesoptionalstring[]highlights: the sentences
actionoptional{ label: string; href: string }highlights: link to the whole conversation
dateoptionalstringAs shown, e.g. Oct 21, 2026
Eingebaute Texte
Diese Texte schreibt Interview selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 11 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| interview | Englisch Interview | Deutsch Interview |
| by | Englisch Questions by {name} | Deutsch Fragen von {name} |
| all | Englisch All voices | Deutsch Alle Stimmen |
| showAnswer | Englisch Show answer | Deutsch Antwort zeigen |
| reveal | Englisch Reveal all answers | Deutsch Alle Antworten zeigen |
| hide | Englisch Hide all answers | Deutsch Alle Antworten verbergen |
| quote | Englisch Quote {a} of {b} | Deutsch Zitat {a} von {b} |
| previous | Englisch Previous quote | Deutsch Vorheriges Zitat |
| next | Englisch Next quote | Deutsch Nächstes Zitat |
| pause | Englisch Pause | Deutsch Anhalten |
| play | Englisch Play | Deutsch Abspielen |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-controls, aria-current, aria-expanded, aria-label, aria-live und aria-pressed, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rolle group.
- Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
- Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 2 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 532 Zeilen
'use client';
import { Fragment, useEffect, useId, useRef, useState } from 'react';
import { cn, useTexts } from '@/lib/kit';
import { Icon } from '@/components/ui/icon';
import { Parallax, Stagger, useScrollFade } from '@/components/ui/motion';
import { Reveal } from '@/components/ui/reveal';
import { Eyebrow } from '@/components/ui/section';
/*
* Interviews, set so that one hears who speaks. Questions and answers are
* plain data; a speaker is named once and recognised by initials after that.
*
* classic question and answer in one column, the initials in the margin,
* a pull quote between the answers
* portrait a large portrait and facts that stay beside the conversation
* round-table several voices; show one voice alone, the others step back
* quick-fire short questions as cards, the answer turns up on press
* highlights the strongest sentences one by one with a progress line,
* then the way to the whole conversation
*/
export type InterviewVariant = 'classic' | 'portrait' | 'round-table' | 'quick-fire' | 'highlights';
export interface InterviewPerson {
name: string;
role?: string;
avatar?: React.ReactNode;
/** Shown in the margin, default the initials */
short?: string;
}
export interface InterviewItem {
/** The question; empty for a statement in a round table */
q?: string;
/** The answer in paragraphs */
a: string[];
/** round-table: index into `people`, who answers */
by?: number;
/** A sentence of the answer set as a pull quote after it */
pull?: string;
}
export interface InterviewProps {
variant?: InterviewVariant;
eyebrow?: React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
/** Who asks */
host?: InterviewPerson;
/** Who answers; round-table: everyone at the table */
people?: InterviewPerson[];
items?: InterviewItem[];
/** portrait: the large picture */
media?: React.ReactNode;
/** portrait: facts, e.g. [{ label: 'Studio', value: 'Graz, since 2014' }] */
facts?: { label: string; value: string }[];
/** highlights: the sentences */
quotes?: string[];
/** highlights: link to the whole conversation */
action?: { label: string; href: string };
/** As shown, e.g. Oct 21, 2026 */
date?: string;
labels?: Partial<Texts>;
className?: string;
}
const TEXTS = {
en: {
interview: 'Interview',
by: 'Questions by {name}',
all: 'All voices',
showAnswer: 'Show answer',
reveal: 'Reveal all answers',
hide: 'Hide all answers',
quote: 'Quote {a} of {b}',
previous: 'Previous quote',
next: 'Next quote',
pause: 'Pause',
play: 'Play',
},
de: {
interview: 'Interview',
by: 'Fragen von {name}',
all: 'Alle Stimmen',
showAnswer: 'Antwort zeigen',
reveal: 'Alle Antworten zeigen',
hide: 'Alle Antworten verbergen',
quote: 'Zitat {a} von {b}',
previous: 'Vorheriges Zitat',
next: 'Nächstes Zitat',
pause: 'Anhalten',
play: 'Abspielen',
},
};
type Texts = (typeof TEXTS)['en'];
/* ------------------------------------------------------------------ */
/* Shared pieces */
/* ------------------------------------------------------------------ */
const initials = (p?: InterviewPerson) =>
p?.short ??
(p?.name ?? '')
.split(' ')
.filter((w) => /^[A-ZÄÖÜ]/.test(w))
.map((w) => w[0])
.slice(0, 2)
.join('');
const ANSWER = 'text-pretty text-[17px] leading-[1.75] text-[color-mix(in_oklab,var(--foreground)_88%,var(--background))] sm:text-[18px]';
const QUESTION = 'text-pretty text-[17px] font-semibold leading-[1.55] text-foreground sm:text-[18px]';
function Face({ p, className }: { p?: InterviewPerson; className?: string }) {
return (
<span data-clip-ok className={cn('grid shrink-0 place-items-center overflow-hidden rounded-full border border-border bg-muted text-[0.38em] font-semibold text-muted-foreground [&>*]:size-full', className)}>
{p?.avatar ?? initials(p)}
</span>
);
}
function PullQuote({ text }: { text: string }) {
return (
<Reveal effect="rise" className="my-12 sm:my-14">
<figure className="border-l-2 border-primary pl-6 sm:pl-8">
<blockquote className="text-balance text-[26px] font-semibold leading-[1.25] tracking-[-0.02em] text-foreground sm:text-[32px]">{text}</blockquote>
</figure>
</Reveal>
);
}
function Head({ eyebrow, title, description, date, people, host, t, center }: InterviewProps & { t: Texts; center?: boolean }) {
const guest = people?.[0];
return (
<header className={cn('max-w-[44rem]', center && 'mx-auto text-center')}>
<Eyebrow className="kit-rise">{eyebrow ?? t.interview}</Eyebrow>
{title && <h2 className="kit-rise text-balance text-h2 font-semibold text-foreground" style={{ '--kit-delay': '60ms' } as React.CSSProperties}>{title}</h2>}
{description && <p className="kit-rise mt-5 text-pretty text-[18px] leading-[1.6] text-muted-foreground sm:text-[20px]" style={{ '--kit-delay': '120ms' } as React.CSSProperties}>{description}</p>}
{(guest || date) && (
<div className={cn('kit-rise mt-7 flex items-center gap-3 border-t border-border pt-6 text-[14px]', center && 'justify-center')} style={{ '--kit-delay': '180ms' } as React.CSSProperties}>
{guest && <Face p={guest} className="size-11" />}
<div className={cn('leading-snug', center && 'text-left')}>
{guest && (
<p className="font-medium text-foreground">
{guest.name}
{guest.role && <span className="font-normal text-muted-foreground">, {guest.role}</span>}
</p>
)}
<p className="mt-0.5 text-muted-foreground">
{[host ? t.by.replace('{name}', host.name) : null, date].filter(Boolean).join(' · ')}
</p>
</div>
</div>
)}
</header>
);
}
/** One exchange: the speaker's initials in the margin on wide screens, above the text on phones */
function Exchange({ item, host, guest, i }: { item: InterviewItem; host?: InterviewPerson; guest?: InterviewPerson; i: number }) {
return (
<div className={cn(i > 0 && 'mt-10')}>
{item.q && (
<Reveal effect="fade" className="grid grid-cols-[minmax(0,1fr)] sm:grid-cols-[3.5rem_minmax(0,1fr)]">
<span className="mb-1 text-[12.5px] font-semibold tracking-[0.04em] text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))] sm:mb-0 sm:pt-[3px]" aria-hidden>
{initials(host)}
</span>
<p className={QUESTION}>
<span className="sr-only">{host?.name}: </span>
{item.q}
</p>
</Reveal>
)}
<div className="mt-4 grid grid-cols-[minmax(0,1fr)] sm:grid-cols-[3.5rem_minmax(0,1fr)]">
<span className="mb-1 text-[12.5px] font-semibold tracking-[0.04em] text-muted-foreground sm:mb-0 sm:pt-[4px]" aria-hidden>
{initials(guest)}
</span>
<div className="space-y-5">
{item.a.map((para, j) => (
<p key={j} className={ANSWER}>
{j === 0 && <span className="sr-only">{guest?.name}: </span>}
{para}
</p>
))}
</div>
</div>
{item.pull && (
<div className="sm:pl-14">
<PullQuote text={item.pull} />
</div>
)}
</div>
);
}
/* ------------------------------------------------------------------ */
/* classic */
/* ------------------------------------------------------------------ */
function Classic(p: InterviewProps & { t: Texts }) {
const guest = p.people?.[0];
return (
<article className={cn('py-16 sm:py-20 lg:py-24', p.className)}>
<div className="kit-container">
<div className="mx-auto max-w-[44rem]">
<Head {...p} />
<div className="mt-12 sm:mt-14">
{(p.items ?? []).map((it, i) => (
<Exchange key={i} item={it} host={p.host} guest={guest} i={i} />
))}
</div>
</div>
</div>
</article>
);
}
/* ------------------------------------------------------------------ */
/* portrait */
/* ------------------------------------------------------------------ */
function Portrait(p: InterviewProps & { t: Texts }) {
const guest = p.people?.[0];
return (
<article className={cn('overflow-x-clip py-16 sm:py-20 lg:py-24', p.className)}>
<div className="kit-container">
<div className="grid grid-cols-[minmax(0,1fr)] gap-10 md:grid-cols-[minmax(0,17rem)_minmax(0,1fr)] md:gap-12 lg:grid-cols-[minmax(0,24rem)_minmax(0,40rem)] lg:justify-center lg:gap-20">
<aside className="md:sticky md:top-10 md:self-start">
<Reveal effect="scale">
<div data-clip-ok className="relative aspect-[5/4] overflow-hidden rounded-2xl md:aspect-[4/5] border border-border bg-muted shadow-[var(--shadow-lg)]">
<Parallax speed={40} className="size-full scale-[1.12] [&>*]:size-full">
{p.media}
</Parallax>
<div className="absolute inset-x-0 bottom-0 bg-[linear-gradient(transparent,rgb(0_0_0/0.62))] p-5 pt-16 text-white">
<p className="text-[19px] font-semibold leading-tight">{guest?.name}</p>
{guest?.role && <p className="mt-0.5 text-[14px] opacity-85">{guest.role}</p>}
</div>
</div>
</Reveal>
{p.facts && p.facts.length > 0 && (
<dl className="mt-6 divide-y divide-border border-y border-border">
{p.facts.map((f, i) => (
<Reveal key={i} effect="fade" delay={i * 60} className="flex items-baseline justify-between gap-4 py-3">
<dt className="text-[13.5px] text-muted-foreground">{f.label}</dt>
<dd className="text-right text-[14.5px] font-medium text-foreground">{f.value}</dd>
</Reveal>
))}
</dl>
)}
</aside>
<div className="min-w-0">
<header>
<Eyebrow className="kit-rise">{p.eyebrow ?? p.t.interview}</Eyebrow>
{p.title && <h2 className="kit-rise text-balance text-h2 font-semibold text-foreground" style={{ '--kit-delay': '60ms' } as React.CSSProperties}>{p.title}</h2>}
{p.description && <p className="kit-rise mt-5 text-pretty text-[18px] leading-[1.6] text-muted-foreground sm:text-[20px]" style={{ '--kit-delay': '120ms' } as React.CSSProperties}>{p.description}</p>}
<p className="kit-rise mt-5 text-[14px] text-muted-foreground" style={{ '--kit-delay': '160ms' } as React.CSSProperties}>
{[p.host ? p.t.by.replace('{name}', p.host.name) : null, p.date].filter(Boolean).join(' · ')}
</p>
</header>
<div className="mt-12 border-t border-border pt-10">
{(p.items ?? []).map((it, i) => (
<div key={i} className={cn(i > 0 && 'mt-12')}>
{it.q && (
<Reveal effect="rise">
<h3 className="text-balance text-[21px] font-semibold leading-[1.35] tracking-[-0.01em] text-foreground sm:text-[23px]">{it.q}</h3>
</Reveal>
)}
<div className="mt-4 space-y-5">
{it.a.map((para, j) => (
<p key={j} className={ANSWER}>
{para}
</p>
))}
</div>
{it.pull && <PullQuote text={it.pull} />}
</div>
))}
</div>
</div>
</div>
</div>
</article>
);
}
/* ------------------------------------------------------------------ */
/* round-table */
/* ------------------------------------------------------------------ */
function RoundTable(p: InterviewProps & { t: Texts }) {
const { t } = p;
const people = p.people ?? [];
const [only, setOnly] = useState<number | null>(null);
const [rowRef, fade] = useScrollFade<HTMLDivElement>();
const items = p.items ?? [];
return (
<article className={cn('py-16 sm:py-20 lg:py-24', p.className)}>
<div className="kit-container">
<div className="grid grid-cols-[minmax(0,1fr)] gap-10 lg:grid-cols-[minmax(0,16rem)_minmax(0,42rem)] lg:justify-center lg:gap-16">
<div className="lg:col-start-2">
<Head {...p} people={undefined} />
</div>
{/* Who is at the table, a filter at the same time */}
<aside className="lg:col-start-1 lg:row-start-2">
<div className="lg:sticky lg:top-10">
<div ref={rowRef} role="group" aria-label={t.all} className={cn('kit-no-scrollbar -mx-6 flex gap-2 overflow-x-auto px-6 lg:mx-0 lg:flex-col lg:overflow-visible lg:px-0', fade)}>
<button type="button" aria-pressed={only === null} onClick={() => setOnly(null)} className={cn('flex min-h-11 shrink-0 items-center gap-3 rounded-xl border px-3 text-left text-[14px] font-medium transition-[background-color,border-color,color] duration-300', only === null ? 'border-border-strong bg-card text-foreground shadow-[var(--shadow-sm)]' : 'border-border text-muted-foreground hover:text-foreground')}>
<span className="flex -space-x-2">
{people.slice(0, 3).map((x, i) => (
<Face key={i} p={x} className="size-7 ring-2 ring-background" />
))}
</span>
{t.all}
</button>
{people.map((x, i) => (
<button key={i} type="button" aria-pressed={only === i} onClick={() => setOnly(only === i ? null : i)} className={cn('flex min-h-11 shrink-0 items-center gap-3 rounded-xl border px-3 py-2 text-left transition-[background-color,border-color,color] duration-300', only === i ? 'border-border-strong bg-card shadow-[var(--shadow-sm)]' : 'border-border hover:border-border-strong')}>
<Face p={x} className="size-9" />
<span className="min-w-0">
<span className="block whitespace-nowrap text-[14px] font-medium text-foreground">{x.name}</span>
{x.role && <span className="block whitespace-nowrap text-[12.5px] text-muted-foreground">{x.role}</span>}
</span>
</button>
))}
</div>
</div>
</aside>
<div className="min-w-0 lg:col-start-2 lg:row-start-2">
{items.map((it, i) => {
const who = it.by !== undefined ? people[it.by] : undefined;
const dim = only !== null && it.q === undefined && it.by !== only;
return (
<div key={i} className={cn('transition-opacity duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', i > 0 && (it.q ? 'mt-12' : 'mt-7'), dim && 'opacity-25')}>
{it.q ? (
<Reveal effect="fade">
<p className={cn(QUESTION, 'border-t border-border pt-8')}>
<span className="mr-2 text-[12.5px] font-semibold tracking-[0.04em] text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]">{initials(p.host)}</span>
<span className="sr-only">{p.host?.name}: </span>
{it.q}
</p>
</Reveal>
) : null}
{it.a.length > 0 && (
<div className={cn('flex gap-4', it.q && 'mt-5')}>
<Face p={who} className="mt-1 size-9" />
<div className="min-w-0 space-y-4">
<p className="text-[13.5px] font-semibold text-foreground">{who?.name}</p>
{it.a.map((para, j) => (
<p key={j} className={cn(ANSWER, '-mt-2 first-of-type:mt-0')}>
{para}
</p>
))}
</div>
</div>
)}
</div>
);
})}
</div>
</div>
</div>
</article>
);
}
/* ------------------------------------------------------------------ */
/* quick-fire */
/* ------------------------------------------------------------------ */
function QuickFire(p: InterviewProps & { t: Texts }) {
const { t } = p;
const uid = useId().replace(/:/g, '');
const items = p.items ?? [];
const [open, setOpen] = useState<Set<number>>(new Set());
const all = open.size === items.length && items.length > 0;
const toggle = (i: number) =>
setOpen((s) => {
const n = new Set(s);
if (n.has(i)) n.delete(i);
else n.add(i);
return n;
});
const guest = p.people?.[0];
return (
<section className={cn('py-16 sm:py-20 lg:py-24', p.className)}>
<div className="kit-container">
<div className="mb-10 flex flex-col gap-6 sm:mb-12 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-[40rem]">
<Eyebrow className="kit-rise">{p.eyebrow ?? t.interview}</Eyebrow>
{p.title && <h2 className="kit-rise text-balance text-h2 font-semibold text-foreground" style={{ '--kit-delay': '60ms' } as React.CSSProperties}>{p.title}</h2>}
{p.description && <p className="kit-rise mt-4 text-pretty text-lead text-muted-foreground" style={{ '--kit-delay': '120ms' } as React.CSSProperties}>{p.description}</p>}
</div>
<button type="button" onClick={() => setOpen(all ? new Set() : new Set(items.map((_, i) => i)))} className="kit-rise inline-flex min-h-11 items-center gap-2 self-start rounded-lg border border-border px-4 text-[14px] font-medium text-foreground transition-colors hover:bg-accent lg:self-end">
<Icon name={all ? 'eye-off' : 'eye'} size={16} />
{all ? t.hide : t.reveal}
</button>
</div>
<Stagger as="ul" step={60} className="grid grid-cols-[minmax(0,1fr)] gap-3 sm:grid-cols-2 lg:grid-cols-3">
{items.map((it, i) => {
const on = open.has(i);
return (
<li key={i}>
<button
type="button"
aria-expanded={on}
aria-controls={`${uid}-a${i}`}
onClick={() => toggle(i)}
className={cn('group/c relative flex h-full min-h-[9rem] w-full flex-col rounded-2xl sm:min-h-[11rem] border p-5 text-left transition-[background-color,border-color,box-shadow,transform] duration-500 ease-[var(--ease-kit)] hover:-translate-y-0.5 sm:p-6', on ? 'border-transparent bg-primary text-primary-foreground shadow-[var(--shadow-lg)]' : 'border-border bg-card shadow-[var(--highlight),var(--shadow-sm)] hover:border-border-strong')}
>
<span className={cn('text-[12.5px] font-semibold tabular-nums transition-colors', on ? 'text-primary-foreground/80' : 'text-muted-foreground')}>{String(i + 1).padStart(2, '0')}</span>
<span className={cn('mt-3 block text-balance text-[19px] font-semibold leading-[1.3] tracking-[-0.01em]', on ? 'text-primary-foreground' : 'text-foreground')}>{it.q}</span>
<span id={`${uid}-a${i}`} className={cn('grid transition-[grid-template-rows,opacity] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', on ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0')}>
<span className="min-h-0 overflow-hidden">
<span className="mt-3 block text-pretty text-[15.5px] leading-[1.55] text-primary-foreground/90">{it.a.join(' ')}</span>
</span>
</span>
<span className={cn('mt-auto flex items-center justify-between pt-5 text-[13px] font-medium', on ? 'text-primary-foreground/85' : 'text-muted-foreground')}>
{on ? guest?.name : t.showAnswer}
<Icon name="plus" size={16} className={cn('transition-transform duration-500 ease-[var(--ease-kit)]', on && 'rotate-45')} />
</span>
</button>
</li>
);
})}
</Stagger>
</div>
</section>
);
}
/* ------------------------------------------------------------------ */
/* highlights */
/* ------------------------------------------------------------------ */
function Highlights(p: InterviewProps & { t: Texts }) {
const { t } = p;
const quotes = p.quotes ?? [];
const guest = p.people?.[0];
const [i, setI] = useState(0);
const [playing, setPlaying] = useState(false);
const DURATION = 6000;
const ref = useRef<HTMLDivElement>(null);
// Start playing once the quotes are in view, never with reduced motion
useEffect(() => {
const el = ref.current;
if (!el || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const io = new IntersectionObserver(([e]) => setPlaying(e.isIntersecting), { threshold: 0.4 });
io.observe(el);
return () => io.disconnect();
}, []);
useEffect(() => {
if (!playing || quotes.length < 2) return;
const id = window.setTimeout(() => setI((n) => (n + 1) % quotes.length), DURATION);
return () => window.clearTimeout(id);
}, [playing, i, quotes.length]);
const go = (n: number) => setI((n + quotes.length) % quotes.length);
const words = (quotes[i] ?? '').split(' ');
return (
<section ref={ref} className={cn('kit-glow relative overflow-x-clip py-16 sm:py-20 lg:py-28', p.className)}>
<div className="kit-container relative">
<div className="mx-auto max-w-[52rem] text-center">
<Eyebrow className="kit-rise">{p.eyebrow ?? t.interview}</Eyebrow>
{p.title && <h2 className="kit-rise text-balance text-[22px] font-semibold text-muted-foreground sm:text-[24px]" style={{ '--kit-delay': '60ms' } as React.CSSProperties}>{p.title}</h2>}
<div className="relative mt-10 grid min-h-[13rem] place-items-center sm:min-h-[12rem]" aria-live="polite">
<blockquote key={i} className="text-balance text-[28px] font-semibold leading-[1.22] tracking-[-0.022em] text-foreground sm:text-[40px] lg:text-[46px]">
<span className="sr-only">{t.quote.replace('{a}', String(i + 1)).replace('{b}', String(quotes.length))}. </span>
{words.map((w, k) => (
<Fragment key={k}>
<span className="kit-rise inline-block" style={{ '--kit-delay': `${k * 35}ms`, animationDuration: '.6s' } as React.CSSProperties}>
{w}
</span>
{k < words.length - 1 ? ' ' : ''}
</Fragment>
))}
</blockquote>
</div>
{guest && (
<div className="mt-8 inline-flex items-center gap-3">
<Face p={guest} className="size-11" />
<p className="text-left text-[14px] leading-snug">
<span className="block font-semibold text-foreground">{guest.name}</span>
{guest.role && <span className="text-muted-foreground">{guest.role}</span>}
</p>
</div>
)}
<div className="mx-auto mt-10 flex max-w-[30rem] items-center gap-3">
<button type="button" onClick={() => go(i - 1)} aria-label={t.previous} className="grid size-11 shrink-0 place-items-center rounded-full border border-border text-foreground transition-colors hover:bg-accent">
<Icon name="arrow-left" size={16} />
</button>
<div className="flex flex-1 gap-1.5">
{quotes.map((_, k) => (
<button key={k} type="button" onClick={() => go(k)} aria-label={t.quote.replace('{a}', String(k + 1)).replace('{b}', String(quotes.length))} aria-current={k === i ? 'true' : undefined} className="group/b relative flex h-11 flex-1 items-center">
<span className="block h-1 w-full overflow-hidden rounded-full bg-border">
<span
key={k === i ? `${i}-${playing}` : k}
className={cn('block h-full origin-left rounded-full bg-primary', k < i ? 'scale-x-100' : k > i ? 'scale-x-0' : playing ? 'motion-safe:animate-kit-progress' : 'scale-x-100')}
style={{ '--kit-duration': `${DURATION}ms` } as React.CSSProperties}
/>
</span>
</button>
))}
</div>
<button type="button" onClick={() => go(i + 1)} aria-label={t.next} className="grid size-11 shrink-0 place-items-center rounded-full border border-border text-foreground transition-colors hover:bg-accent">
<Icon name="arrow-right" size={16} />
</button>
<button type="button" onClick={() => setPlaying((v) => !v)} aria-label={playing ? t.pause : t.play} aria-pressed={!playing} className="grid size-11 shrink-0 place-items-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground">
<Icon name={playing ? 'pause' : 'play'} size={15} />
</button>
</div>
{p.action && (
<a href={p.action.href} className="group/a mt-10 inline-flex min-h-11 items-center gap-2 text-[15px] font-medium text-foreground">
{p.action.label}
<Icon name="arrow-right" size={16} className="transition-transform duration-300 group-hover/a:translate-x-0.5" />
</a>
)}
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------ */
export function Interview(props: InterviewProps) {
const t = useTexts(TEXTS, props.labels);
const { variant = 'classic' } = props;
if (variant === 'portrait') return <Portrait {...props} t={t} />;
if (variant === 'round-table') return <RoundTable {...props} t={t} />;
if (variant === 'quick-fire') return <QuickFire {...props} t={t} />;
if (variant === 'highlights') return <Highlights {...props} t={t} />;
return <Classic {...props} t={t} />;
}Häufige Fragen
Wie installiere ich Interview?
Mit aiw add @ai-website/interview. Der Befehl legt die Datei components/ui/interview.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon, reveal, motion und section. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Interview?
5 Varianten, Frage und Antwort, Porträt, Gesprächsrunde, Schnelle Fragen und Höhepunkte. Du wählst sie mit der Prop variant, zum Beispiel variant="portrait".
Kann ich die Texte von Interview übersetzen?
Ja. Interview bringt 11 eigene Texte auf Deutsch und Englisch mit, etwa „Interview“, „Alle Stimmen“ und „Antwort zeigen“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Interview einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Interview mit aiw_components und installieren sie mit aiw_add @ai-website/interview. Nenn im Prompt die Variante, etwa variant="classic", dann schreibt der Assistent deine Inhalte direkt in die Props.
Kann ich Interview ohne Code anpassen?
Ja, im Studio. Alle 5 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden Interview-Code zum Kopieren.