Compare commits

...

5 Commits

Author SHA1 Message Date
dc4b737daa chore(main): release 0.16.0 (#711) 2025-10-24 16:18:49 -06:00
Igor Monadical
0baff7abf7 transcript ui copy button placement (#712)
Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
2025-10-24 16:52:02 -04:00
Igor Monadical
962c40e2b6 feat: search date filter (#710)
* search date filter

* search date filter

* search date filter

* search date filter

* pr comment

---------

Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
2025-10-23 20:16:43 -04:00
Igor Monadical
3c4b9f2103 chore: error reporting and naming (#708)
* chore: error reporting and naming

* chore: error reporting and naming

---------

Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
2025-10-22 13:45:08 -04:00
Igor Monadical
c6c035aacf removal of email-verified from /me (#707)
Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com>
2025-10-21 14:49:33 -04:00
16 changed files with 462 additions and 117 deletions

View File

@@ -1,4 +1,4 @@
name: Deploy to Amazon ECS
name: Build container/push to container registry
on: [workflow_dispatch]

View File

@@ -1,5 +1,12 @@
# Changelog
## [0.16.0](https://github.com/Monadical-SAS/reflector/compare/v0.15.0...v0.16.0) (2025-10-24)
### Features
* search date filter ([#710](https://github.com/Monadical-SAS/reflector/issues/710)) ([962c40e](https://github.com/Monadical-SAS/reflector/commit/962c40e2b6428ac42fd10aea926782d7a6f3f902))
## [0.15.0](https://github.com/Monadical-SAS/reflector/compare/v0.14.0...v0.15.0) (2025-10-20)

View File

@@ -135,6 +135,8 @@ class SearchParameters(BaseModel):
user_id: str | None = None
room_id: str | None = None
source_kind: SourceKind | None = None
from_datetime: datetime | None = None
to_datetime: datetime | None = None
class SearchResultDB(BaseModel):
@@ -402,6 +404,14 @@ class SearchController:
base_query = base_query.where(
transcripts.c.source_kind == params.source_kind
)
if params.from_datetime:
base_query = base_query.where(
transcripts.c.created_at >= params.from_datetime
)
if params.to_datetime:
base_query = base_query.where(
transcripts.c.created_at <= params.to_datetime
)
if params.query_text is not None:
order_by = sqlalchemy.desc(sqlalchemy.text("rank"))

View File

@@ -426,7 +426,12 @@ async def task_pipeline_file_process(*, transcript_id: str):
await pipeline.process(audio_file)
except Exception:
except Exception as e:
logger.error(
f"File pipeline failed for transcript {transcript_id}: {type(e).__name__}: {str(e)}",
exc_info=True,
transcript_id=transcript_id,
)
await pipeline.set_status(transcript_id, "error")
raise

View File

@@ -56,6 +56,16 @@ class FileTranscriptModalProcessor(FileTranscriptProcessor):
},
follow_redirects=True,
)
if response.status_code != 200:
error_body = response.text
self.logger.error(
"Modal API error",
audio_url=data.audio_url,
status_code=response.status_code,
error_body=error_body,
)
response.raise_for_status()
result = response.json()

View File

@@ -34,8 +34,16 @@ TOPIC_PROMPT = dedent(
class TopicResponse(BaseModel):
"""Structured response for topic detection"""
title: str = Field(description="A descriptive title for the topic being discussed")
summary: str = Field(description="A concise 1-2 sentence summary of the discussion")
title: str = Field(
description="A descriptive title for the topic being discussed",
validation_alias="Title",
)
summary: str = Field(
description="A concise 1-2 sentence summary of the discussion",
validation_alias="Summary",
)
model_config = {"populate_by_name": True}
class TranscriptTopicDetectorProcessor(Processor):

View File

@@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi_pagination import Page
from fastapi_pagination.ext.databases import apaginate
from jose import jwt
from pydantic import BaseModel, Field, constr, field_serializer
from pydantic import AwareDatetime, BaseModel, Field, constr, field_serializer
import reflector.auth as auth
from reflector.db import get_database
@@ -133,6 +133,21 @@ SearchOffsetParam = Annotated[
SearchOffsetBase, Query(description="Number of results to skip")
]
SearchFromDatetimeParam = Annotated[
AwareDatetime | None,
Query(
alias="from",
description="Filter transcripts created on or after this datetime (ISO 8601 with timezone)",
),
]
SearchToDatetimeParam = Annotated[
AwareDatetime | None,
Query(
alias="to",
description="Filter transcripts created on or before this datetime (ISO 8601 with timezone)",
),
]
class SearchResponse(BaseModel):
results: list[SearchResult]
@@ -174,18 +189,23 @@ async def transcripts_search(
offset: SearchOffsetParam = 0,
room_id: Optional[str] = None,
source_kind: Optional[SourceKind] = None,
from_datetime: SearchFromDatetimeParam = None,
to_datetime: SearchToDatetimeParam = None,
user: Annotated[
Optional[auth.UserInfo], Depends(auth.current_user_optional)
] = None,
):
"""
Full-text search across transcript titles and content.
"""
"""Full-text search across transcript titles and content."""
if not user and not settings.PUBLIC_MODE:
raise HTTPException(status_code=401, detail="Not authenticated")
user_id = user["sub"] if user else None
if from_datetime and to_datetime and from_datetime > to_datetime:
raise HTTPException(
status_code=400, detail="'from' must be less than or equal to 'to'"
)
search_params = SearchParameters(
query_text=parse_search_query_param(q),
limit=limit,
@@ -193,6 +213,8 @@ async def transcripts_search(
user_id=user_id,
room_id=room_id,
source_kind=source_kind,
from_datetime=from_datetime,
to_datetime=to_datetime,
)
results, total = await search_controller.search_transcripts(search_params)

View File

@@ -11,7 +11,6 @@ router = APIRouter()
class UserInfo(BaseModel):
sub: str
email: Optional[str]
email_verified: Optional[bool]
@router.get("/me")

View File

@@ -0,0 +1,256 @@
from datetime import datetime, timedelta, timezone
import pytest
from reflector.db import get_database
from reflector.db.search import SearchParameters, search_controller
from reflector.db.transcripts import SourceKind, transcripts
@pytest.mark.asyncio
class TestDateRangeIntegration:
async def setup_test_transcripts(self):
# Use a test user_id that will match in our search parameters
test_user_id = "test-user-123"
test_data = [
{
"id": "test-before-range",
"created_at": datetime(2024, 1, 15, tzinfo=timezone.utc),
"title": "Before Range Transcript",
"user_id": test_user_id,
},
{
"id": "test-start-boundary",
"created_at": datetime(2024, 6, 1, tzinfo=timezone.utc),
"title": "Start Boundary Transcript",
"user_id": test_user_id,
},
{
"id": "test-middle-range",
"created_at": datetime(2024, 6, 15, tzinfo=timezone.utc),
"title": "Middle Range Transcript",
"user_id": test_user_id,
},
{
"id": "test-end-boundary",
"created_at": datetime(2024, 6, 30, 23, 59, 59, tzinfo=timezone.utc),
"title": "End Boundary Transcript",
"user_id": test_user_id,
},
{
"id": "test-after-range",
"created_at": datetime(2024, 12, 31, tzinfo=timezone.utc),
"title": "After Range Transcript",
"user_id": test_user_id,
},
]
for data in test_data:
full_data = {
"id": data["id"],
"name": data["id"],
"status": "ended",
"locked": False,
"duration": 60.0,
"created_at": data["created_at"],
"title": data["title"],
"short_summary": "Test summary",
"long_summary": "Test long summary",
"share_mode": "public",
"source_kind": SourceKind.FILE,
"audio_deleted": False,
"reviewed": False,
"user_id": data["user_id"],
}
await get_database().execute(transcripts.insert().values(**full_data))
return test_data
async def cleanup_test_transcripts(self, test_data):
"""Clean up test transcripts."""
for data in test_data:
await get_database().execute(
transcripts.delete().where(transcripts.c.id == data["id"])
)
@pytest.mark.asyncio
async def test_filter_with_from_datetime_only(self):
"""Test filtering with only from_datetime parameter."""
test_data = await self.setup_test_transcripts()
test_user_id = "test-user-123"
try:
params = SearchParameters(
query_text=None,
from_datetime=datetime(2024, 6, 1, tzinfo=timezone.utc),
to_datetime=None,
user_id=test_user_id,
)
results, total = await search_controller.search_transcripts(params)
# Should include: start_boundary, middle, end_boundary, after
result_ids = [r.id for r in results]
assert "test-before-range" not in result_ids
assert "test-start-boundary" in result_ids
assert "test-middle-range" in result_ids
assert "test-end-boundary" in result_ids
assert "test-after-range" in result_ids
finally:
await self.cleanup_test_transcripts(test_data)
@pytest.mark.asyncio
async def test_filter_with_to_datetime_only(self):
"""Test filtering with only to_datetime parameter."""
test_data = await self.setup_test_transcripts()
test_user_id = "test-user-123"
try:
params = SearchParameters(
query_text=None,
from_datetime=None,
to_datetime=datetime(2024, 6, 30, tzinfo=timezone.utc),
user_id=test_user_id,
)
results, total = await search_controller.search_transcripts(params)
result_ids = [r.id for r in results]
assert "test-before-range" in result_ids
assert "test-start-boundary" in result_ids
assert "test-middle-range" in result_ids
assert "test-end-boundary" not in result_ids
assert "test-after-range" not in result_ids
finally:
await self.cleanup_test_transcripts(test_data)
@pytest.mark.asyncio
async def test_filter_with_both_datetimes(self):
test_data = await self.setup_test_transcripts()
test_user_id = "test-user-123"
try:
params = SearchParameters(
query_text=None,
from_datetime=datetime(2024, 6, 1, tzinfo=timezone.utc),
to_datetime=datetime(
2024, 7, 1, tzinfo=timezone.utc
), # Inclusive of 6/30
user_id=test_user_id,
)
results, total = await search_controller.search_transcripts(params)
result_ids = [r.id for r in results]
assert "test-before-range" not in result_ids
assert "test-start-boundary" in result_ids
assert "test-middle-range" in result_ids
assert "test-end-boundary" in result_ids
assert "test-after-range" not in result_ids
finally:
await self.cleanup_test_transcripts(test_data)
@pytest.mark.asyncio
async def test_date_filter_with_room_and_source_kind(self):
test_data = await self.setup_test_transcripts()
test_user_id = "test-user-123"
try:
params = SearchParameters(
query_text=None,
from_datetime=datetime(2024, 6, 1, tzinfo=timezone.utc),
to_datetime=datetime(2024, 7, 1, tzinfo=timezone.utc),
source_kind=SourceKind.FILE,
room_id=None,
user_id=test_user_id,
)
results, total = await search_controller.search_transcripts(params)
for result in results:
assert result.source_kind == SourceKind.FILE
assert result.created_at >= datetime(2024, 6, 1, tzinfo=timezone.utc)
assert result.created_at <= datetime(2024, 7, 1, tzinfo=timezone.utc)
finally:
await self.cleanup_test_transcripts(test_data)
@pytest.mark.asyncio
async def test_empty_results_for_future_dates(self):
test_data = await self.setup_test_transcripts()
test_user_id = "test-user-123"
try:
params = SearchParameters(
query_text=None,
from_datetime=datetime(2099, 1, 1, tzinfo=timezone.utc),
to_datetime=datetime(2099, 12, 31, tzinfo=timezone.utc),
user_id=test_user_id,
)
results, total = await search_controller.search_transcripts(params)
assert results == []
assert total == 0
finally:
await self.cleanup_test_transcripts(test_data)
@pytest.mark.asyncio
async def test_date_only_input_handling(self):
test_data = await self.setup_test_transcripts()
test_user_id = "test-user-123"
try:
# Pydantic will parse date-only strings to datetime at midnight
from_dt = datetime(2024, 6, 15, 0, 0, 0, tzinfo=timezone.utc)
to_dt = datetime(2024, 6, 16, 0, 0, 0, tzinfo=timezone.utc)
params = SearchParameters(
query_text=None,
from_datetime=from_dt,
to_datetime=to_dt,
user_id=test_user_id,
)
results, total = await search_controller.search_transcripts(params)
result_ids = [r.id for r in results]
assert "test-middle-range" in result_ids
assert "test-before-range" not in result_ids
assert "test-after-range" not in result_ids
finally:
await self.cleanup_test_transcripts(test_data)
class TestDateValidationEdgeCases:
"""Edge case tests for datetime validation."""
def test_timezone_aware_comparison(self):
"""Test that timezone-aware comparisons work correctly."""
# PST time (UTC-8)
pst = timezone(timedelta(hours=-8))
pst_dt = datetime(2024, 6, 15, 8, 0, 0, tzinfo=pst)
# UTC time equivalent (8AM PST = 4PM UTC)
utc_dt = datetime(2024, 6, 15, 16, 0, 0, tzinfo=timezone.utc)
assert pst_dt == utc_dt
def test_mixed_timezone_input(self):
"""Test handling mixed timezone inputs."""
pst = timezone(timedelta(hours=-8))
ist = timezone(timedelta(hours=5, minutes=30))
from_date = datetime(2024, 6, 15, 0, 0, 0, tzinfo=pst) # PST midnight
to_date = datetime(2024, 6, 15, 23, 59, 59, tzinfo=ist) # IST end of day
assert from_date.tzinfo is not None
assert to_date.tzinfo is not None
assert from_date < to_date

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import React from "react";
import Markdown from "react-markdown";
import "../../../styles/markdown.css";
@@ -16,17 +16,15 @@ import {
} from "@chakra-ui/react";
import { LuPen } from "react-icons/lu";
import { useError } from "../../../(errors)/errorContext";
import ShareAndPrivacy from "../shareAndPrivacy";
type FinalSummaryProps = {
transcriptResponse: GetTranscript;
topicsResponse: GetTranscriptTopic[];
onUpdate?: (newSummary) => void;
transcript: GetTranscript;
topics: GetTranscriptTopic[];
onUpdate: (newSummary: string) => void;
finalSummaryRef: React.Dispatch<React.SetStateAction<HTMLDivElement | null>>;
};
export default function FinalSummary(props: FinalSummaryProps) {
const finalSummaryRef = useRef<HTMLParagraphElement>(null);
const [isEditMode, setIsEditMode] = useState(false);
const [preEditSummary, setPreEditSummary] = useState("");
const [editedSummary, setEditedSummary] = useState("");
@@ -35,10 +33,10 @@ export default function FinalSummary(props: FinalSummaryProps) {
const updateTranscriptMutation = useTranscriptUpdate();
useEffect(() => {
setEditedSummary(props.transcriptResponse?.long_summary || "");
}, [props.transcriptResponse?.long_summary]);
setEditedSummary(props.transcript?.long_summary || "");
}, [props.transcript?.long_summary]);
if (!props.topicsResponse || !props.transcriptResponse) {
if (!props.topics || !props.transcript) {
return null;
}
@@ -54,9 +52,7 @@ export default function FinalSummary(props: FinalSummaryProps) {
long_summary: newSummary,
},
});
if (props.onUpdate) {
props.onUpdate(newSummary);
}
console.log("Updated long summary:", updatedTranscript);
} catch (err) {
console.error("Failed to update long summary:", err);
@@ -75,7 +71,7 @@ export default function FinalSummary(props: FinalSummaryProps) {
};
const onSaveClick = () => {
updateSummary(editedSummary, props.transcriptResponse.id);
updateSummary(editedSummary, props.transcript.id);
setIsEditMode(false);
};
@@ -133,11 +129,6 @@ export default function FinalSummary(props: FinalSummaryProps) {
>
<LuPen />
</IconButton>
<ShareAndPrivacy
finalSummaryRef={finalSummaryRef}
transcriptResponse={props.transcriptResponse}
topicsResponse={props.topicsResponse}
/>
</>
)}
</Flex>
@@ -153,7 +144,7 @@ export default function FinalSummary(props: FinalSummaryProps) {
mt={2}
/>
) : (
<div ref={finalSummaryRef} className="markdown">
<div ref={props.finalSummaryRef} className="markdown">
<Markdown>{editedSummary}</Markdown>
</div>
)}

View File

@@ -41,6 +41,8 @@ export default function TranscriptDetails(details: TranscriptDetails) {
waiting || mp3.audioDeleted === true,
);
const useActiveTopic = useState<Topic | null>(null);
const [finalSummaryElement, setFinalSummaryElement] =
useState<HTMLDivElement | null>(null);
useEffect(() => {
if (waiting) {
@@ -124,9 +126,12 @@ export default function TranscriptDetails(details: TranscriptDetails) {
<TranscriptTitle
title={transcript.data?.title || "Unnamed Transcript"}
transcriptId={transcriptId}
onUpdate={(newTitle) => {
onUpdate={() => {
transcript.refetch().then(() => {});
}}
transcript={transcript.data || null}
topics={topics.topics}
finalSummaryElement={finalSummaryElement}
/>
</Flex>
{mp3.audioDeleted && (
@@ -148,11 +153,12 @@ export default function TranscriptDetails(details: TranscriptDetails) {
{transcript.data && topics.topics ? (
<>
<FinalSummary
transcriptResponse={transcript.data}
topicsResponse={topics.topics}
transcript={transcript.data}
topics={topics.topics}
onUpdate={() => {
transcript.refetch();
transcript.refetch().then(() => {});
}}
finalSummaryRef={setFinalSummaryElement}
/>
</>
) : (

View File

@@ -26,9 +26,9 @@ import { useAuth } from "../../lib/AuthProvider";
import { featureEnabled } from "../../lib/features";
type ShareAndPrivacyProps = {
finalSummaryRef: any;
transcriptResponse: GetTranscript;
topicsResponse: GetTranscriptTopic[];
finalSummaryElement: HTMLDivElement | null;
transcript: GetTranscript;
topics: GetTranscriptTopic[];
};
type ShareOption = { value: ShareMode; label: string };
@@ -48,7 +48,7 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
const [isOwner, setIsOwner] = useState(false);
const [shareMode, setShareMode] = useState<ShareOption>(
shareOptionsData.find(
(option) => option.value === props.transcriptResponse.share_mode,
(option) => option.value === props.transcript.share_mode,
) || shareOptionsData[0],
);
const [shareLoading, setShareLoading] = useState(false);
@@ -70,7 +70,7 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
try {
const updatedTranscript = await updateTranscriptMutation.mutateAsync({
params: {
path: { transcript_id: props.transcriptResponse.id },
path: { transcript_id: props.transcript.id },
},
body: requestBody,
});
@@ -90,8 +90,8 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
const userId = auth.status === "authenticated" ? auth.user?.id : null;
useEffect(() => {
setIsOwner(!!(requireLogin && userId === props.transcriptResponse.user_id));
}, [userId, props.transcriptResponse.user_id]);
setIsOwner(!!(requireLogin && userId === props.transcript.user_id));
}, [userId, props.transcript.user_id]);
return (
<>
@@ -171,19 +171,19 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
<Flex gap={2} mb={2}>
{requireLogin && (
<ShareZulip
transcriptResponse={props.transcriptResponse}
topicsResponse={props.topicsResponse}
transcript={props.transcript}
topics={props.topics}
disabled={toShareMode(shareMode.value) === "private"}
/>
)}
<ShareCopy
finalSummaryRef={props.finalSummaryRef}
transcriptResponse={props.transcriptResponse}
topicsResponse={props.topicsResponse}
finalSummaryElement={props.finalSummaryElement}
transcript={props.transcript}
topics={props.topics}
/>
</Flex>
<ShareLink transcriptId={props.transcriptResponse.id} />
<ShareLink transcriptId={props.transcript.id} />
</Dialog.Body>
</Dialog.Content>
</Dialog.Positioner>

View File

@@ -5,34 +5,35 @@ type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"];
import { Button, BoxProps, Box } from "@chakra-ui/react";
type ShareCopyProps = {
finalSummaryRef: any;
transcriptResponse: GetTranscript;
topicsResponse: GetTranscriptTopic[];
finalSummaryElement: HTMLDivElement | null;
transcript: GetTranscript;
topics: GetTranscriptTopic[];
};
export default function ShareCopy({
finalSummaryRef,
transcriptResponse,
topicsResponse,
finalSummaryElement,
transcript,
topics,
...boxProps
}: ShareCopyProps & BoxProps) {
const [isCopiedSummary, setIsCopiedSummary] = useState(false);
const [isCopiedTranscript, setIsCopiedTranscript] = useState(false);
const onCopySummaryClick = () => {
let text_to_copy = finalSummaryRef.current?.innerText;
const text_to_copy = finalSummaryElement?.innerText;
text_to_copy &&
if (text_to_copy) {
navigator.clipboard.writeText(text_to_copy).then(() => {
setIsCopiedSummary(true);
// Reset the copied state after 2 seconds
setTimeout(() => setIsCopiedSummary(false), 2000);
});
}
};
const onCopyTranscriptClick = () => {
let text_to_copy =
topicsResponse
topics
?.map((topic) => topic.transcript)
.join("\n\n")
.replace(/ +/g, " ")

View File

@@ -26,8 +26,8 @@ import {
import { featureEnabled } from "../../lib/features";
type ShareZulipProps = {
transcriptResponse: GetTranscript;
topicsResponse: GetTranscriptTopic[];
transcript: GetTranscript;
topics: GetTranscriptTopic[];
disabled: boolean;
};
@@ -88,14 +88,14 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
}, [stream, streams]);
const handleSendToZulip = async () => {
if (!props.transcriptResponse) return;
if (!props.transcript) return;
if (stream && topic) {
try {
await postToZulipMutation.mutateAsync({
params: {
path: {
transcript_id: props.transcriptResponse.id,
transcript_id: props.transcript.id,
},
query: {
stream,

View File

@@ -2,14 +2,22 @@ import { useState } from "react";
import type { components } from "../../reflector-api";
type UpdateTranscript = components["schemas"]["UpdateTranscript"];
type GetTranscript = components["schemas"]["GetTranscript"];
type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"];
import { useTranscriptUpdate } from "../../lib/apiHooks";
import { Heading, IconButton, Input, Flex, Spacer } from "@chakra-ui/react";
import { LuPen } from "react-icons/lu";
import ShareAndPrivacy from "./shareAndPrivacy";
type TranscriptTitle = {
title: string;
transcriptId: string;
onUpdate?: (newTitle: string) => void;
onUpdate: (newTitle: string) => void;
// share props
transcript: GetTranscript | null;
topics: GetTranscriptTopic[] | null;
finalSummaryElement: HTMLDivElement | null;
};
const TranscriptTitle = (props: TranscriptTitle) => {
@@ -29,9 +37,7 @@ const TranscriptTitle = (props: TranscriptTitle) => {
},
body: requestBody,
});
if (props.onUpdate) {
props.onUpdate(newTitle);
}
console.log("Updated transcript title:", newTitle);
} catch (err) {
console.error("Failed to update transcript:", err);
@@ -62,11 +68,11 @@ const TranscriptTitle = (props: TranscriptTitle) => {
}
setIsEditing(false);
};
const handleChange = (e) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setDisplayedTitle(e.target.value);
};
const handleKeyDown = (e) => {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
updateTitle(displayedTitle, props.transcriptId);
setIsEditing(false);
@@ -111,6 +117,13 @@ const TranscriptTitle = (props: TranscriptTitle) => {
>
<LuPen />
</IconButton>
{props.transcript && props.topics && (
<ShareAndPrivacy
finalSummaryElement={props.finalSummaryElement}
transcript={props.transcript}
topics={props.topics}
/>
)}
</Flex>
)}
</>

View File

@@ -604,25 +604,25 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/user/tokens": {
"/v1/user/api-keys": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** List Tokens */
get: operations["v1_list_tokens"];
/** List Api Keys */
get: operations["v1_list_api_keys"];
put?: never;
/** Create Token */
post: operations["v1_create_token"];
/** Create Api Key */
post: operations["v1_create_api_key"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/user/tokens/{token_id}": {
"/v1/user/api-keys/{key_id}": {
parameters: {
query?: never;
header?: never;
@@ -632,8 +632,8 @@ export interface paths {
get?: never;
put?: never;
post?: never;
/** Delete Token */
delete: operations["v1_delete_token"];
/** Delete Api Key */
delete: operations["v1_delete_api_key"];
options?: never;
head?: never;
patch?: never;
@@ -700,6 +700,26 @@ export interface paths {
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** ApiKeyResponse */
ApiKeyResponse: {
/**
* Id
* @description A non-empty string
*/
id: string;
/**
* User Id
* @description A non-empty string
*/
user_id: string;
/** Name */
name: string | null;
/**
* Created At
* Format: date-time
*/
created_at: string;
};
/** AudioWaveform */
AudioWaveform: {
/** Data */
@@ -759,6 +779,36 @@ export interface components {
*/
updated_at: string;
};
/** CreateApiKeyRequest */
CreateApiKeyRequest: {
/** Name */
name?: string | null;
};
/** CreateApiKeyResponse */
CreateApiKeyResponse: {
/**
* Id
* @description A non-empty string
*/
id: string;
/**
* User Id
* @description A non-empty string
*/
user_id: string;
/** Name */
name: string | null;
/**
* Created At
* Format: date-time
*/
created_at: string;
/**
* Key
* @description A non-empty string
*/
key: string;
};
/** CreateParticipant */
CreateParticipant: {
/** Speaker */
@@ -811,27 +861,6 @@ export interface components {
*/
allow_duplicated: boolean | null;
};
/** CreateTokenRequest */
CreateTokenRequest: {
/** Name */
name?: string | null;
};
/** CreateTokenResponse */
CreateTokenResponse: {
/** Id */
id: string;
/** User Id */
user_id: string;
/** Name */
name: string | null;
/**
* Created At
* Format: date-time
*/
created_at: string;
/** Token */
token: string;
};
/** CreateTranscript */
CreateTranscript: {
/** Name */
@@ -1425,20 +1454,6 @@ export interface components {
* @enum {string}
*/
SyncStatus: "success" | "unchanged" | "error" | "skipped";
/** TokenResponse */
TokenResponse: {
/** Id */
id: string;
/** User Id */
user_id: string;
/** Name */
name: string | null;
/**
* Created At
* Format: date-time
*/
created_at: string;
};
/** Topic */
Topic: {
/** Name */
@@ -1518,8 +1533,6 @@ export interface components {
sub: string;
/** Email */
email: string | null;
/** Email Verified */
email_verified: boolean | null;
};
/** ValidationError */
ValidationError: {
@@ -2265,6 +2278,10 @@ export interface operations {
offset?: number;
room_id?: string | null;
source_kind?: components["schemas"]["SourceKind"] | null;
/** @description Filter transcripts created on or after this datetime (ISO 8601 with timezone) */
from?: string | null;
/** @description Filter transcripts created on or before this datetime (ISO 8601 with timezone) */
to?: string | null;
};
header?: never;
path?: never;
@@ -3006,7 +3023,7 @@ export interface operations {
};
};
};
v1_list_tokens: {
v1_list_api_keys: {
parameters: {
query?: never;
header?: never;
@@ -3021,12 +3038,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TokenResponse"][];
"application/json": components["schemas"]["ApiKeyResponse"][];
};
};
};
};
v1_create_token: {
v1_create_api_key: {
parameters: {
query?: never;
header?: never;
@@ -3035,7 +3052,7 @@ export interface operations {
};
requestBody: {
content: {
"application/json": components["schemas"]["CreateTokenRequest"];
"application/json": components["schemas"]["CreateApiKeyRequest"];
};
};
responses: {
@@ -3045,7 +3062,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CreateTokenResponse"];
"application/json": components["schemas"]["CreateApiKeyResponse"];
};
};
/** @description Validation Error */
@@ -3059,12 +3076,12 @@ export interface operations {
};
};
};
v1_delete_token: {
v1_delete_api_key: {
parameters: {
query?: never;
header?: never;
path: {
token_id: string;
key_id: string;
};
cookie?: never;
};