Perplexity: Natywny design wyszukiwania AI

Jak Perplexity zbudowalo zaufanie w wyszukiwaniu AI: design z priorytetem cytatow, stopniowe ujawnianie, odpowiedzi strumieniowe i udoskonalanie zapytan. Kod w zestawie.

5 min czytania • 1080 słów
Perplexity: Natywny design wyszukiwania AI screenshot

„Ludzie chcą dostać informacje jak najszybciej i chcą móc tym informacjom ufać. A skoro tak, to w gruncie rzeczy jest to całkiem prosty problem projektowy.”

Filozofia

Perplexity reprezentuje nowy paradygmat: wyszukiwanie natywnie oparte na AI. Tradycyjna wyszukiwarka zwraca listę linków, chatbot odpowiada w formie rozmowy, a Perplexity syntetyzuje informacje z wielu źródeł w jedną odpowiedź popartą cytowaniami. Projekt ucieleśnia radykalną przejrzystość — każde twierdzenie da się prześledzić aż do źródła.

Kluczowa obserwacja zespołu: wyszukiwanie nie jest rozmową. To misja zdobycia informacji. Interfejs to odzwierciedla, pokazując źródła i odpowiedzi, a nie historię czatu i persony.


Najważniejsze wnioski

  1. Cytowania są niezbędne dla zaufania do AI - Każde twierdzenie faktyczne prowadzi do swojego źródła; panel źródeł pozostaje widoczny, więc użytkownicy mogą weryfikować informacje, nie opuszczając odpowiedzi
  2. Znajomy interfejs obniża bariery - Pole wyszukiwania wygląda jak Google, a nie jak prompt czatu; użytkownicy mogą wpisać słowa kluczowe albo pełne pytania, bez uczenia się wzorców konwersacyjnych
  3. Przewidywanie pytań uzupełniających - Większość użytkowników nie wie, o co zapytać dalej; podsuwanie pytań uzupełniających trafnych w danym kontekście podtrzymuje ich zaangażowanie
  4. Pokazywanie procesu, nie tylko wyników - Fazy „Wyszukiwanie → Czytanie → Pisanie” dzięki przejrzystości budują zaufanie i skracają odczuwany czas oczekiwania
  5. Słabe wyniki jako porażki - Gdy zapytanie dałoby niewystarczające wyniki, lepiej poprosić o doprecyzowanie, niż zwracać przeciętne odpowiedzi

Biblioteka wzorców

Projektowanie z cytowaniami na pierwszym planie

To Perplexity spopularyzowało cytowania inline w odpowiedziach AI, zasadniczo zmieniając to, jak użytkownicy ufają generowanym treściom. Każde twierdzenie faktyczne prowadzi do swojego źródła.

interface Citation {
  index: number;
  url: string;
  title: string;
  favicon: string;
  snippet: string;
  domain: string;
}

interface AnswerBlock {
  text: string;
  citations: number[];  // Indices into citation array
}

function CitedAnswer({ blocks, citations }: {
  blocks: AnswerBlock[];
  citations: Citation[];
}) {
  return (
    <article className="answer">
      {blocks.map((block, i) => (
        <p key={i}>
          {block.text}
          {block.citations.map(citationIndex => (
            <CitationMarker
              key={citationIndex}
              citation={citations[citationIndex]}
              index={citationIndex + 1}
            />
          ))}
        </p>
      ))}

      {/* Source panel always visible */}
      <aside className="sources-panel">
        <h3>Sources</h3>
        {citations.map((citation, i) => (
          <SourceCard key={i} citation={citation} index={i + 1} />
        ))}
      </aside>
    </article>
  );
}

function CitationMarker({ citation, index }: {
  citation: Citation;
  index: number;
}) {
  const [expanded, setExpanded] = useState(false);

  return (
    <span className="citation-wrapper">
      <sup
        className="citation-marker"
        onMouseEnter={() => setExpanded(true)}
        onMouseLeave={() => setExpanded(false)}
      >
        [{index}]
      </sup>

      {/* Expandable snippet preview */}
      {expanded && (
        <div className="citation-preview">
          <img src={citation.favicon} alt="" className="citation-favicon" />
          <span className="citation-domain">{citation.domain}</span>
          <p className="citation-snippet">{citation.snippet}</p>
        </div>
      )}
    </span>
  );
}

Wniosek projektowy: Cytowania nie są opcjonalne. Są wbudowane w każdą interakcję. Panel źródeł pozostaje widoczny, dzięki czemu użytkownicy mogą weryfikować twierdzenia bez opuszczania widoku odpowiedzi.


Znajomy interfejs wyszukiwania

Pole wejściowe Perplexity wygląda jak tradycyjne pole wyszukiwania, a nie jak prompt czatu. Znajomy wygląd obniża próg wejścia dla użytkowników, którzy nie czują się swobodnie z konwersacyjnym AI.

function SearchInput({ onSubmit }: { onSubmit: (query: string) => void }) {
  const [query, setQuery] = useState('');

  return (
    <div className="search-container">
      {/* Intentionally looks like Google/traditional search */}
      <div className="search-box">
        <SearchIcon className="search-icon" />

        <input
          type="text"
          className="search-input"
          placeholder="Ask anything..."
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && onSubmit(query)}
        />

        {query && (
          <button
            className="clear-button"
            onClick={() => setQuery('')}
          >
            <XIcon />
          </button>
        )}

        <button
          className="submit-button"
          onClick={() => onSubmit(query)}
        >
          <ArrowRightIcon />
        </button>
      </div>

      {/* Optional context chips */}
      <div className="focus-chips">
        <Chip icon={<GlobeIcon />}>All</Chip>
        <Chip icon={<AcademicIcon />}>Academic</Chip>
        <Chip icon={<CodeIcon />}>Code</Chip>
        <Chip icon={<VideoIcon />}>Video</Chip>
      </div>
    </div>
  );
}

Wniosek projektowy: Wystarczy nawet kilka słów kluczowych. Użytkownicy nie muszą układać rozbudowanych promptów. Interfejs przyjmuje zarówno proste zapytania, jak i złożone pytania.


Stopniowe odsłanianie przez pytania uzupełniające

Zamiast oczekiwać, że użytkownicy sami zadadzą świetne pytania uzupełniające, Perplexity je przewiduje i podsuwa. Przewidywanie pytań uzupełniających odpowiada na prosty fakt: większość użytkowników nie wie, o co zapytać dalej.

interface FollowUpSuggestion {
  question: string;
  reasoning: string;  // Why this might be relevant
}

function FollowUpSuggestions({
  suggestions,
  onSelect
}: {
  suggestions: FollowUpSuggestion[];
  onSelect: (question: string) => void;
}) {
  return (
    <div className="follow-ups">
      <h4>Related</h4>

      {/* Show one at a time - progressive disclosure */}
      {suggestions.slice(0, 4).map((suggestion, i) => (
        <button
          key={i}
          className="follow-up-chip"
          onClick={() => onSelect(suggestion.question)}
        >
          <span className="follow-up-text">{suggestion.question}</span>
          <ArrowRightIcon className="follow-up-arrow" />
        </button>
      ))}
    </div>
  );
}

// Predict follow-ups based on query context
function generateFollowUps(query: string, answer: string): FollowUpSuggestion[] {
  // AI generates contextually relevant next questions
  return [
    { question: "How does this compare to alternatives?", reasoning: "comparison" },
    { question: "What are the limitations?", reasoning: "critical analysis" },
    { question: "Can you provide specific examples?", reasoning: "concrete details" },
    { question: "What's the historical context?", reasoning: "background" },
  ];
}

Wniosek projektowy: Podobnie jak niewiele osób zadaje pytania na koniec prezentacji typu keynote, większość użytkowników nie radzi sobie najlepiej z zadawaniem pytań uzupełniających. Warto przewidzieć, czego mogą chcieć się dowiedzieć.


Strumieniowanie odpowiedzi ze stopniowym renderowaniem

Perplexity strumieniuje odpowiedzi za pomocą Server-Sent Events (SSE), ale interfejs odsłania treść stopniowo, w sposób, który wydaje się naturalny.

function StreamingAnswer({ query }: { query: string }) {
  const [sources, setSources] = useState<Citation[]>([]);
  const [answer, setAnswer] = useState<string>('');
  const [phase, setPhase] = useState<'searching' | 'reading' | 'writing'>('searching');

  useEffect(() => {
    const eventSource = new EventSource(`/api/search?q=${encodeURIComponent(query)}`);

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);

      switch (data.type) {
        case 'sources':
          setPhase('reading');
          setSources(data.sources);
          break;
        case 'chunk':
          setPhase('writing');
          setAnswer(prev => prev + data.text);
          break;
        case 'done':
          eventSource.close();
          break;
      }
    };

    return () => eventSource.close();
  }, [query]);

  return (
    <div className="streaming-answer">
      {/* Phase indicator */}
      <PhaseIndicator phase={phase} />

      {/* Sources appear first */}
      {sources.length > 0 && (
        <SourceCards sources={sources} />
      )}

      {/* Answer streams in */}
      <div className="answer-content">
        <TypewriterText text={answer} />
        {phase === 'writing' && <BlinkingCursor />}
      </div>
    </div>
  );
}

function PhaseIndicator({ phase }: { phase: 'searching' | 'reading' | 'writing' }) {
  const phases = {
    searching: { icon: <SearchIcon />, text: 'Searching the web...' },
    reading: { icon: <BookIcon />, text: 'Reading sources...' },
    writing: { icon: <PenIcon />, text: 'Writing answer...' },
  };

  return (
    <div className="phase-indicator">
      {phases[phase].icon}
      <span>{phases[phase].text}</span>
      <LoadingDots />
    </div>
  );
}

Wniosek projektowy: Należy pokazywać proces, nie tylko wynik. Użytkownicy najpierw widzą pojawiające się źródła (co buduje zaufanie), a potem obserwują, jak powstaje odpowiedź. Przejrzystość zmniejsza niepokój w czasie oczekiwania.


Zapobieganie błędom przez doprecyzowanie

Gdy zapytanie jest zbyt ogólne, Perplexity prosi o wyjaśnienie, zamiast zwracać słabe wyniki. System traktuje niewystarczające wyniki jako stan porażki.

interface ClarificationRequest {
  type: 'ambiguous' | 'too_broad' | 'missing_context';
  suggestions: string[];
  originalQuery: string;
}

function QueryRefinement({ request, onRefine }: {
  request: ClarificationRequest;
  onRefine: (refinedQuery: string) => void;
}) {
  const messages = {
    ambiguous: "I found multiple meanings. Which one did you mean?",
    too_broad: "This topic is quite broad. Can you be more specific?",
    missing_context: "I need a bit more context to give you a useful answer.",
  };

  return (
    <div className="refinement-prompt">
      <p className="refinement-message">{messages[request.type]}</p>

      <div className="refinement-suggestions">
        {request.suggestions.map((suggestion, i) => (
          <button
            key={i}
            className="refinement-option"
            onClick={() => onRefine(suggestion)}
          >
            {suggestion}
          </button>
        ))}
      </div>

      <div className="refinement-custom">
        <input
          type="text"
          placeholder="Or type your own refinement..."
          onKeyDown={(e) => {
            if (e.key === 'Enter') {
              onRefine((e.target as HTMLInputElement).value);
            }
          }}
        />
      </div>
    </div>
  );
}

Wniosek projektowy: Inne wyszukiwarki zwracają słabe wyniki i liczą na to, że użytkownicy sami doprecyzują zapytanie. Perplexity aktywnie prowadzi użytkowników ku lepszym zapytaniom.


Collections i Spaces

Do uporządkowanej pracy badawczej Perplexity oferuje Spaces — dedykowane kolekcje, w których użytkownicy mogą organizować zapytania, przypinać wyniki i przesyłać materiały referencyjne.

interface Space {
  id: string;
  name: string;
  threads: Thread[];
  documents: Document[];
  createdAt: Date;
}

interface Thread {
  id: string;
  query: string;
  answer: Answer;
  citations: Citation[];
  pinned: boolean;
}

function SpacesSidebar({ spaces, activeSpace, onSelect }: {
  spaces: Space[];
  activeSpace: string;
  onSelect: (id: string) => void;
}) {
  return (
    <aside className="spaces-sidebar">
      <header>
        <h2>Spaces</h2>
        <button className="new-space">
          <PlusIcon /> New Space
        </button>
      </header>

      <nav className="space-list">
        {spaces.map(space => (
          <button
            key={space.id}
            className={`space-item ${space.id === activeSpace ? 'active' : ''}`}
            onClick={() => onSelect(space.id)}
          >
            <FolderIcon />
            <span className="space-name">{space.name}</span>
            <span className="thread-count">{space.threads.length}</span>
          </button>
        ))}
      </nav>
    </aside>
  );
}

function SpaceView({ space }: { space: Space }) {
  return (
    <div className="space-view">
      <header className="space-header">
        <h1>{space.name}</h1>
        <div className="space-actions">
          <button><ShareIcon /> Share</button>
          <button><UploadIcon /> Add Files</button>
        </div>
      </header>

      {/* Pinned threads */}
      <section className="pinned-threads">
        <h3>Pinned</h3>
        {space.threads.filter(t => t.pinned).map(thread => (
          <ThreadCard key={thread.id} thread={thread} />
        ))}
      </section>

      {/* All threads */}
      <section className="all-threads">
        <h3>All Threads</h3>
        {space.threads.map(thread => (
          <ThreadCard key={thread.id} thread={thread} />
        ))}
      </section>

      {/* Embedded documents */}
      {space.documents.length > 0 && (
        <section className="space-documents">
          <h3>Reference Materials</h3>
          {space.documents.map(doc => (
            <DocumentCard key={doc.id} document={doc} />
          ))}
        </section>
      )}
    </div>
  );
}

Wniosek projektowy: Collections odzwierciedlają stosy otwartych kart z materiałami badawczymi, które naukowcy i dziennikarze utrzymują ręcznie — tyle że teraz są wbudowane bezpośrednio w produkt.


System projektowania wizualnego

Typografia: przejrzystość ponad osobowość

Perplexity stosuje neutralny, bardzo czytelny system typograficzny. Gwiazdą jest treść, nie interfejs.

:root {
  /* System font stack for fastest load */
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-mono: 'SF Mono', Monaco, 'Courier New', monospace;

  /* Type scale - optimized for reading */
  --text-sm: 13px;
  --text-base: 15px;
  --text-lg: 17px;
  --text-xl: 20px;
  --text-2xl: 24px;

  /* Line heights - generous for readability */
  --leading-tight: 1.3;
  --leading-normal: 1.6;
  --leading-relaxed: 1.8;
}

/* Answer text - optimized for long-form reading */
.answer-content {
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  max-width: 680px;  /* Optimal reading width */
}

/* Citations - smaller, unobtrusive */
.citation-marker {
  font-size: var(--text-sm);
  color: var(--accent-primary);
  cursor: pointer;
  vertical-align: super;
}

/* Source cards - scannable metadata */
.source-card {
  font-size: var(--text-sm);
  line-height: var(--leading-tight);
}

.source-domain {
  font-family: var(--font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

System kolorów: zaufanie przez powściągliwość

Kolor jest używany oszczędnie, głównie w cytowaniach, wskaźnikach statusu i stanach fokusu.

:root {
  /* Neutral foundation */
  --bg-primary: #ffffff;
  --bg-secondary: #f7f7f8;
  --bg-tertiary: #ededef;

  /* Text hierarchy */
  --text-primary: #1a1a1b;
  --text-secondary: #57575a;
  --text-tertiary: #8e8e93;

  /* Citation accent - trustworthy blue */
  --accent-citation: #0066cc;
  --accent-citation-hover: #0052a3;

  /* Focus states */
  --focus-ring: 0 0 0 2px var(--accent-citation);

  /* Phase indicators */
  --phase-searching: #f59e0b;
  --phase-reading: #3b82f6;
  --phase-writing: #10b981;
}

/* Clean, ad-free interface */
.search-results {
  background: var(--bg-primary);
  padding: 24px;
  border-radius: 12px;
}

/* Citation hover state */
.citation-marker:hover {
  background: var(--accent-citation);
  color: white;
  border-radius: 2px;
}

/* Source card - subtle boundary */
.source-card {
  border: 1px solid var(--bg-tertiary);
  border-radius: 8px;
  padding: 12px;
  transition: border-color 0.15s ease;
}

.source-card:hover {
  border-color: var(--accent-citation);
}

Wzorce animacji

Przejścia między fazami

Płynne przejścia między fazami wyszukiwania → czytania → pisania pozwalają użytkownikom na bieżąco wiedzieć, co się dzieje.

/* Phase indicator */
.phase-indicator {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: var(--bg-secondary);
  border-radius: 20px;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* Loading dots animation */
.loading-dots {
  display: flex;
  gap: 4px;
}

.loading-dots span {
  width: 4px;
  height: 4px;
  background: var(--text-tertiary);
  border-radius: 50%;
  animation: dot-pulse 1.4s infinite ease-in-out;
}

.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }

@keyframes dot-pulse {
  0%, 80%, 100% {
    transform: scale(0.6);
    opacity: 0.5;
  }
  40% {
    transform: scale(1);
    opacity: 1;
  }
}

/* Sources fade in as they're found */
.source-card {
  animation: fade-in-up 0.3s ease-out;
}

@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Stagger source cards */
.source-card:nth-child(1) { animation-delay: 0ms; }
.source-card:nth-child(2) { animation-delay: 100ms; }
.source-card:nth-child(3) { animation-delay: 200ms; }
.source-card:nth-child(4) { animation-delay: 300ms; }

Strumieniowy tekst

Odpowiedź pojawia się znak po znaku, ale w pogrupowanych fragmentach, co poprawia czytelność.

function TypewriterText({ text }: { text: string }) {
  const [displayedText, setDisplayedText] = useState('');

  useEffect(() => {
    // Text already arrived from SSE - just display it
    setDisplayedText(text);
  }, [text]);

  return (
    <div className="typewriter">
      {displayedText}
    </div>
  );
}

function BlinkingCursor() {
  return (
    <span className="cursor" aria-hidden="true">|</span>
  );
}
.cursor {
  animation: blink 1s step-end infinite;
  color: var(--text-secondary);
}

@keyframes blink {
  50% { opacity: 0; }
}

Wyciągnięte wnioski

1. Cytowania są niezbędne dla zaufania do AI

W interfejsach AI każde twierdzenie faktyczne powinno dać się prześledzić do źródła. Perplexity wbudowuje wierność cytowań w każdą interakcję. Nie jest ona ani opcjonalna, ani ukryta.

2. Pole wyszukiwania > prompt czatu

Znajomy interfejs wyszukiwania obniża bariery. Użytkownicy mogą wpisać słowa kluczowe albo pełne pytania — działa jedno i drugie. Nie należy wymuszać wzorców konwersacyjnych.

3. Przewidywanie pytań uzupełniających

Większość użytkowników nie wie, o co zapytać dalej. Lepiej podsuwać pytania uzupełniające trafne w danym kontekście, niż oczekiwać, że to użytkownicy poprowadzą rozmowę.

4. Pokazywanie procesu

Strumieniowanie daje przejrzystość, a nie tylko szybkość. Pokazywanie faz „Wyszukiwanie → Czytanie → Pisanie” buduje zaufanie i skraca odczuwany czas oczekiwania.

5. Słabe wyniki jako porażki

Gdy zapytanie dałoby niewystarczające wyniki, należy poprosić o doprecyzowanie, zamiast zwracać słabe odpowiedzi. Warto prowadzić użytkowników ku lepszym zapytaniom.

6. Czysty interfejs = sygnał zaufania

Minimalistyczny interfejs bez reklam sygnalizuje, że produkt stawia jakość informacji ponad monetyzację.


Najczęściej zadawane pytania

Jak działa system cytowań Perplexity?

Każde twierdzenie faktyczne w odpowiedzi Perplexity zawiera numerowane cytowania inline, prowadzące do adresów URL źródeł. Panel źródeł pozostaje widoczny obok odpowiedzi i pokazuje favicon, domenę oraz podgląd fragmentu. Po najechaniu kursorem na numer cytowania rozwija się podgląd z konkretnym kontekstem źródła. Dzięki temu weryfikacja jest natychmiastowa i nie wymaga od użytkowników przekopywania się przez wyniki.

Dlaczego Perplexity wygląda jak wyszukiwarka, a nie jak chatbot?

Zespół doszedł do wniosku, że wyszukiwanie nie jest rozmową — to misja zdobycia informacji. Znajomy interfejs z polem wyszukiwania przyjmuje zarówno proste słowa kluczowe, jak i złożone pytania, nie wymagając od użytkowników nauki konwersacyjnych wzorców promptowania. Obniża to barierę dla osób, które nie czują się swobodnie z interfejsami czatu AI.

Jakie jest podejście Perplexity do strumieniowania odpowiedzi?

Perplexity używa Server-Sent Events (SSE), aby stopniowo odsłaniać treść w trzech fazach: „Wyszukiwanie” (znajdowanie źródeł), „Czytanie” (analiza źródeł) i „Pisanie” (synteza odpowiedzi). Źródła pojawiają się jako pierwsze, by zbudować zaufanie, zanim zacznie się strumieniowanie odpowiedzi. Ta przejrzystość zmniejsza niepokój w czasie oczekiwania.

Jak Perplexity radzi sobie z niejednoznacznymi lub zbyt ogólnymi zapytaniami?

Gdy zapytanie dałoby słabe wyniki, Perplexity prosi o wyjaśnienie, zamiast zwracać przeciętne odpowiedzi. System rozpoznaje niejednoznaczne terminy, zbyt szerokie tematy lub brakujący kontekst, a następnie proponuje konkretne doprecyzowania. Użytkownicy mogą kliknąć jedną z sugestii albo wpisać własne wyjaśnienie.

Czym są Spaces w Perplexity i jak się z nich korzysta?

Spaces to dedykowane kolekcje do uporządkowanej pracy badawczej, w których użytkownicy mogą grupować powiązane zapytania, przypinać ważne wyniki i przesyłać dokumenty referencyjne. Odzwierciedlają stosy otwartych kart z materiałami badawczymi, które naukowcy i dziennikarze utrzymują ręcznie, tyle że teraz są wbudowane bezpośrednio w produkt i służą do prowadzenia trwających projektów badawczych.


Źródła