Solutions

Resources

Features

May 22, 2025

DevLog: AI-Powered Inline Rephrase with OpenAI in Nuxt 3, TipTap 3 & Tailwind 2

Empower your users to refine any text selection or full‐document content without leaving the editor. We’ll add an inline “Rephrase” button backed by OpenAI’s chat endpoint.

Prerequisites

  • Vue 3 project

  • TipTap 3 installed (@tiptap/vue-3 + @tiptap/starter-kit + @tiptap/extension-bubble-menu)

  • Composition API (<script setup>)

  • An environment variable OPENAI_API_KEY set to your OpenAI secret key

  • Nuxt 3 (Nitro) or any Node server to proxy the OpenAI call

1. Editor Initialization

// inside your component
import { useEditor, BubbleMenu, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'

const editor = useEditor({
  extensions: [
    StarterKit,
    BubbleMenu,
  ],
  content: '<p>Select text and click “Rephrase”!</p>',
})

2. BubbleMenu Markup

<template>
  <bubble-menu
    v-if="editor"
    :editor="editor"
    class="rounded-lg bg-white shadow p-2 flex items-center space-x-2"
    :tippy-options="{ duration: 100, arrow: false }"
  >
    <template v-if="!isEditing">
      <button @click="startRephrase" :disabled="loading">
        {{ loading ? '…' : 'Rephrase' }}
      </button>
    </template>

    <template v-else>
      <input
        v-model="instructions"
        placeholder="Add instructions…"
        @keyup.enter="runRephrase"
        class="border rounded px-2 py-1 flex-1"
      />
      <button @click="runRephrase" :disabled="loading">
        {{ loading ? '…' : 'Go' }}
      </button>
      <button @click="cancelRephrase">×</button>
    </template>
  </bubble-menu>

  <editor-content :editor="editor" />
</template>

3. State & Methods

<script setup lang="ts">
import { ref } from 'vue'
import { useEditor } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import BubbleMenu from '@tiptap/extension-bubble-menu'
import { rephraseText } from '~/utils/ai'

const editor = useEditor({
  extensions: [ StarterKit, BubbleMenu ],
  content: '<p>Select text and click “Rephrase”!</p>',
})

const loading = ref(false)
const isEditing = ref(false)
const instructions = ref('')
const originalText = ref('')
const aiResult = ref('')
const selectionRange = ref<{ from: number | null, to: number | null }>({
  from: null,
  to: null,
})

function startRephrase() {
  const { state } = editor.value!
  const { from, to, empty } = state.selection

  if (empty) {
    originalText.value = state.doc.textContent
    selectionRange.value = { from: null, to: null }
  } else {
    originalText.value = state.doc.textBetween(from, to, '\n\n')
    selectionRange.value = { from, to }
  }

  isEditing.value = true
  instructions.value = ''
}

function cancelRephrase() {
  isEditing.value = false
  instructions.value = ''
}

async function runRephrase() {
  if (loading.value) return

  loading.value = true
  try {
    aiResult.value = await rephraseText(originalText.value, instructions.value)
    applyRephrase()
  } catch (err) {
    console.error('Rephrase failed:', err)
  } finally {
    loading.value = false
    isEditing.value = false
  }
}

function applyRephrase() {
  const ed = editor.value!
  if (selectionRange.value.from != null) {
    ed.chain()
      .focus()
      .insertContentAt(
        { from: selectionRange.value.from, to: selectionRange.value.to! },
        aiResult.value,
      )
      .run()
  } else {
    ed.chain().focus().setContent(aiResult.value).run()
  }
}
</script>

4. Front-End Helper: rephraseText()

// src/utils/ai.ts

/**
 * Call our server endpoint to hit OpenAI’s chat API.
 */
export async function rephraseText(
  text: string,
  instructions: string = ''
): Promise<string> {
  const res = await fetch('/api/rephrase', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text, instructions }),
  })

  if (!res.ok) {
    throw new Error(`API error ${res.status}: ${res.statusText}`)
  }

  const { rephrased } = (await res.json()) as { rephrased: string }
  return rephrased
}

5. Server Handler: OpenAI Chat

// server/api/rephrase.post.ts
import { readBody } from 'h3'
import OpenAI from 'openai'

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

export default defineEventHandler(async (event) => {
  const { text, instructions } = await readBody<{
    text: string
    instructions: string
  }>(event)

  const messages = [
    {
      role: 'system',
      content:
        'You are a helpful assistant that rephrases text without adding code fences or extra markdown.',
    },
    {
      role: 'user',
      content: [
        `Please rephrase the following text:`,
        `"${text}"`,
        `Additional instructions: ${instructions || '(none)'}`,
      ].join('\n\n'),
    },
  ]

  const resp = await openai.chat.completions.create({
    model: 'gpt-3.5-turbo',
    messages,
    temperature: 0.7,
  })

  const rephrased =
    resp.choices?.[0]?.message?.content?.trim() ?? text

  return { rephrased }
})

6. Styling & UX Tips

  • Disable buttons while loading is true

  • Show a spinner or “Loading…” text

  • Surface errors with a toast or inline banner

  • Add proper aria-labels and focus outlines

  • Tweak Tailwind classes for your brand’s look

With this setup, users can select text, add custom AI instructions, and get instant rephrases—powered by OpenAI’s chat endpoint—right inside your Vue 3 + TipTap 3 editor.

Every voice matters

At SurveyNoodle, we believe that every voice holds the power to transform your business. By truly listening, you can spark innovation and turn feedback into actionable insights.

Get Started Now

Every voice matters

At SurveyNoodle, we believe that every voice holds the power to transform your business. By truly listening, you can spark innovation and turn feedback into actionable insights.

Get Started Now

Every voice matters

At SurveyNoodle, we believe that every voice holds the power to transform your business. By truly listening, you can spark innovation and turn feedback into actionable insights.

Get Started Now

Every voice matters

At SurveyNoodle, we believe that every voice holds the power to transform your business. By truly listening, you can spark innovation and turn feedback into actionable insights.

Get Started Now