Skip to content

Commit

Permalink
Merge pull request #148 from FlaminSarge/summary
Browse files Browse the repository at this point in the history
Show YT's AI chat summary in a 'pinned'-style box if it is present
  • Loading branch information
KentoNishi authored Dec 16, 2024
2 parents 836aca3 + f8155be commit b88ab55
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 8 deletions.
71 changes: 71 additions & 0 deletions src/components/ChatSummary.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script lang="ts">
import { slide, fade } from 'svelte/transition';
import MessageRun from './MessageRuns.svelte';
import Tooltip from './common/Tooltip.svelte';
import Icon from 'smelte/src/components/Icon';
import { Theme } from '../ts/chat-constants';
import { createEventDispatcher } from 'svelte';
export let summary: Ytc.ParsedSummary;
let dismissed = false;
let shorten = false;
const classes = 'rounded inline-flex flex-col overflow-visible ' +
'bg-secondary-900 p-2 w-full text-white z-10 shadow';
const onShorten = () => { shorten = !shorten; };
$: if (summary) {
dismissed = false;
shorten = false;
}
const dispatch = createEventDispatcher();
$: dismissed, shorten, dispatch('resize');
</script>

{#if !dismissed}
<div
class={classes}
transition:fade={{ duration: 250 }}
>
<div class="flex flex-row items-center cursor-pointer" on:click={onShorten}>
<div class="font-medium tracking-wide text-white flex-1">
<span class="mr-1 inline-block" style="transform: translateY(3px);">
<Icon small>
{#if shorten}
expand_more
{:else}
expand_less
{/if}
</Icon>
</span>
{#each summary.item.header as run}
{#if run.type === 'text'}
<span class="align-middle">{run.text}</span>
{/if}
{/each}
</div>
<div class="flex-none self-end" style="transform: translateY(3px);">
<Tooltip offsetY={0} small>
<Icon
slot="activator"
class="cursor-pointer text-lg"
on:click={() => { dismissed = true; }}
>
close
</Icon>
Dismiss
</Tooltip>
</div>
</div>
{#if !shorten && !dismissed}
<div class="mt-1 whitespace-pre-line" transition:slide|local={{ duration: 300 }}>
<MessageRun runs={summary.item.subheader} deleted forceDark forceTLColor={Theme.DARK}/>
</div>
<div class="mt-1 whitespace-pre-line" transition:slide|local={{ duration: 300 }}>
<MessageRun runs={summary.item.message} forceDark forceTLColor={Theme.DARK}/>
</div>
{/if}
</div>
{/if}
21 changes: 17 additions & 4 deletions src/components/Hyperchat.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import WelcomeMessage from './WelcomeMessage.svelte';
import Message from './Message.svelte';
import PinnedMessage from './PinnedMessage.svelte';
import ChatSummary from './ChatSummary.svelte';
import PaidMessage from './PaidMessage.svelte';
import MembershipItem from './MembershipItem.svelte';
import ReportBanDialog from './ReportBanDialog.svelte';
Expand All @@ -28,6 +29,7 @@
showUsernames,
showTimestamps,
showUserBadges,
showChatSummary,
refreshScroll,
emojiRenderMode,
useSystemEmojis,
Expand All @@ -53,6 +55,7 @@
let messageActions: (Chat.MessageAction | Welcome)[] = [];
const messageKeys = new Set<string>();
let pinned: Ytc.ParsedPinned | null;
let summary: Ytc.ParsedSummary | null;
let div: HTMLElement;
let isAtBottom = true;
let truncateInterval: number;
Expand Down Expand Up @@ -183,6 +186,9 @@
case 'delete':
onDelete(action.deletion);
break;
case 'summary':
summary = action;
break;
case 'pin':
pinned = action;
break;
Expand Down Expand Up @@ -393,11 +399,18 @@
</div>
{/each}
</div>
{#if pinned}
{#if (summary && $showChatSummary) || pinned}
<div class="absolute top-0 w-full" bind:this={topBar}>
<div class="mx-1.5 mt-1.5">
<PinnedMessage pinned={pinned} on:resize={topBarResized} />
</div>
{#if summary && $showChatSummary}
<div class="mx-1.5 mt-1.5">
<ChatSummary summary={summary} on:resize={topBarResized} />
</div>
{/if}
{#if pinned}
<div class="mx-1.5 mt-1.5">
<PinnedMessage pinned={pinned} on:resize={topBarResized} />
</div>
{/if}
</div>
{/if}
{#if !isAtBottom}
Expand Down
4 changes: 3 additions & 1 deletion src/components/settings/InterfaceSettings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
useSystemEmojis,
isDark,
enableStickySuperchatBar,
enableHighlightedMentions
enableHighlightedMentions,
showChatSummary
} from '../../ts/storage';
import { themeItems, emojiRenderItems } from '../../ts/chat-constants';
import Card from '../common/Card.svelte';
Expand Down Expand Up @@ -59,6 +60,7 @@
<Checkbox name="Show timestamps" store={showTimestamps} />
<Checkbox name="Show usernames" store={showUsernames} />
<Checkbox name="Show user badges" store={showUserBadges} />
<Checkbox name="Show experimental chat summaries by YouTube" store={showChatSummary} />
<Checkbox name="Highlight mentions" store={enableHighlightedMentions} />
</Card>

Expand Down
52 changes: 50 additions & 2 deletions src/ts/chat-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,51 @@ const parseMessageRuns = (runs?: Ytc.MessageRun[]): Ytc.ParsedRun[] => {
return parsedRuns;
};

// takes an array of runs, finds newline-only runs, and splits the array by them, up to maxSplit times
// final output will have maximum length of maxSplit + 1
// maxSplit = -1 will have no limit for splits
const splitRunsByNewline = (runs: Ytc.ParsedRun[], maxSplit: number = -1): Ytc.ParsedRun[][] =>
runs.reduce((acc: Ytc.ParsedRun[][], run: Ytc.ParsedRun) => {
if (run.type === 'text' && run.text === '\n' && (maxSplit == -1 || acc.length <= maxSplit)) {
acc.push([]);
} else {
acc[acc.length - 1].push(run);
}
return acc;
}, [[]]);

const parseChatSummary = (renderer: Ytc.AddChatItem, isEphemeral: boolean, bannerTimeoutMs: number): Ytc.ParsedSummary | undefined => {
if (!renderer.liveChatBannerChatSummaryRenderer) {
return;
}
const baseRenderer = renderer.liveChatBannerChatSummaryRenderer!;
const runs = parseMessageRuns(renderer.liveChatBannerChatSummaryRenderer?.chatSummary.runs);
const splitRuns = splitRunsByNewline(runs, 2);
if (splitRuns.length < 3) {
// YT probably changed the format, refuse to do anything to avoid breaking
return;
}
const subheader = splitRuns[1].map(run => {
if (run.type === 'text') {
// turn subheader into a link to YT's support page detailing the AI summary feature
return { type: 'link', text: run.text, url: 'https://support.google.com/youtube/thread/18138167?msgid=284199217' } as Ytc.ParsedLinkRun;
} else {
return run;
}
});
const item: Ytc.ParsedSummary = {
type: 'summary',
item: {
header: splitRuns[0],
subheader: subheader,
message: splitRuns[2],
},
id: baseRenderer.liveChatSummaryId,
showtime: isEphemeral ? (bannerTimeoutMs / 1000) : 0,
};
return item;
}

const parseAddChatItemAction = (action: Ytc.AddChatItemAction, isReplay = false, liveTimeoutOrReplayMs = 0): Ytc.ParsedMessage | undefined => {
const actionItem = action.item;
const renderer = actionItem.liveChatTextMessageRenderer ??
Expand Down Expand Up @@ -183,8 +228,11 @@ const parseMessageDeletedAction = (action: Ytc.MessageDeletedAction): Ytc.Parsed
};
};

const parsePinnedMessageAction = (action: Ytc.AddPinnedAction): Ytc.ParsedPinned | undefined => {
const parseBannerAction = (action: Ytc.AddPinnedAction): Ytc.ParsedPinned | Ytc.ParsedSummary | undefined => {
const baseRenderer = action.bannerRenderer.liveChatBannerRenderer;
if (baseRenderer.contents.liveChatBannerChatSummaryRenderer) {
return parseChatSummary(baseRenderer.contents, action.bannerProperties?.isEphemeral ?? false, action.bannerProperties?.bannerTimeoutMs ?? 0);
}
const parsedContents = parseAddChatItemAction(
{ item: baseRenderer.contents }, true
);
Expand Down Expand Up @@ -229,7 +277,7 @@ const processCommonAction = (
if (action.addChatItemAction) {
return parseAddChatItemAction(action.addChatItemAction, isReplay, liveTimeoutOrReplayMs);
} else if (action.addBannerToLiveChatCommand) {
return parsePinnedMessageAction(action.addBannerToLiveChatCommand);
return parseBannerAction(action.addBannerToLiveChatCommand);
} else if (action.removeBannerForLiveChatCommand) {
return { type: 'unpin' } as const;
} else if (action.addLiveChatTickerItemAction) {
Expand Down
1 change: 1 addition & 0 deletions src/ts/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const showProfileIcons = stores.addSyncStore('hc.messages.showProfileIcon
export const showUsernames = stores.addSyncStore('hc.messages.showUsernames', true);
export const showTimestamps = stores.addSyncStore('hc.messages.showTimestamps', false);
export const showUserBadges = stores.addSyncStore('hc.messages.showUserBadges', true);
export const showChatSummary = stores.addSyncStore('hc.messages.showChatSummary', true);
export const lastClosedVersion = stores.addSyncStore('hc.lastClosedVersion', '');
export const showOnlyMemberChat = stores.addSyncStore('hc.showOnlyMemberChat', false);
export const emojiRenderMode = stores.addSyncStore('hc.emojiRenderMode', YoutubeEmojiRenderMode.SHOW_ALL);
Expand Down
28 changes: 27 additions & 1 deletion src/ts/typings/ytc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ declare namespace Ytc {
};
};
};
bannerProperties?: {
isEphemeral: boolean;
bannerTimeoutMs: number;
}
}

interface AddTickerAction {
Expand Down Expand Up @@ -227,6 +231,15 @@ declare namespace Ytc {
};
}

interface ChatSummaryRenderer {
liveChatSummaryId: string;
chatSummary: RunsObj;
icon?: {
/** Unlocalized string */
iconType: string;
};
}

interface PlaceholderRenderer { // No idea what the purpose of this is
id: string;
timestampUsec: IntString;
Expand All @@ -248,6 +261,8 @@ declare namespace Ytc {
liveChatSponsorshipsGiftPurchaseAnnouncementRenderer?: MembershipGiftPurchaseRenderer;
/** Membership gift redemption */
liveChatSponsorshipsGiftRedemptionAnnouncementRenderer?: TextMessageRenderer;
/** AI Chat Summary */
liveChatBannerChatSummaryRenderer?: ChatSummaryRenderer;
/** ??? */
liveChatPlaceholderItemRenderer?: PlaceholderRenderer;
}
Expand Down Expand Up @@ -365,13 +380,24 @@ declare namespace Ytc {
};
}

interface ParsedSummary {
type: 'summary';
item: {
header: ParsedRun[];
subheader: ParsedRun[];
message: ParsedRun[];
};
id: string;
showtime: number;
}

interface ParsedTicker extends ParsedMessage {
type: 'ticker';
tickerDuration: number;
detailText?: string;
}

type ParsedMisc = ParsedPinned | { type: 'unpin'};
type ParsedMisc = ParsedPinned | ParsedSummary | { type: 'unpin' };

type ParsedTimedItem = ParsedMessage | ParsedTicker;

Expand Down

0 comments on commit b88ab55

Please sign in to comment.