diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/Workbook.ts b/Clients/Xamarin.Interactive.Client.Web/ClientApp/Workbook.ts index 1465e09af..6de83b949 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/Workbook.ts +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/Workbook.ts @@ -39,6 +39,16 @@ export async function loadWorkbookFromString(workbookSession: WorkbookSession, f return new Workbook(workbookSession, fileName, content, data) } +export async function loadWorkbookFromUrl(workbookSession: WorkbookSession, url: string): Promise { + var response = await fetch(url) + + if (!response.ok) + throw new Error("Couldn't load the workbook") + + var content = await response.text() + return await loadWorkbookFromString (workbookSession, url, content) +} + export async function loadWorkbookFromGist(workbookSession: WorkbookSession, gistUrl: string) : Promise { // TODO: Specify revision to load, specify root workbook file. const gistId = gistUrl.split('/').slice(-1) @@ -55,7 +65,7 @@ export async function loadWorkbookFromGist(workbookSession: WorkbookSession, gis return await loadWorkbookFromWorkbookPackage(workbookSession, workbookPackage); } -export async function loadWorkbookFromWorkbookPackage(workbookSession: WorkbookSession, workbookPackage: File): Promise { +export async function loadWorkbookFromWorkbookPackage(workbookSession: WorkbookSession, workbookPackage: File): Promise { const loadedZip = await loadAsync(workbookPackage) const workbookFiles = loadedZip.filter((path, file) => path.endsWith(".workbook")); diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Home.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Home.tsx index 0bc0d730f..e173b5acb 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Home.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Home.tsx @@ -11,6 +11,6 @@ import { WorkbookShell } from './WorkbookShell'; export class Home extends React.Component, {}> { public render() { - return + return } } \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Layout.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Layout.tsx index 8f8d1b0a2..899f45a58 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Layout.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/Layout.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Fabric } from 'office-ui-fabric-react/lib/Fabric'; import { initializeIcons } from '@uifabric/icons'; +import 'office-ui-fabric-react/dist/css/fabric.css'; initializeIcons(); diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookShell.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookShell.tsx index 8a0a688d9..b88fdc2cf 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookShell.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/components/WorkbookShell.tsx @@ -21,18 +21,23 @@ import { StatusMessageBar } from './StatusMessageBar' import { StatusUIAction, MessageKind, MessageSeverity } from '../messages' import './WorkbookShell.scss' -import { loadWorkbookFromString, loadWorkbookFromWorkbookPackage, loadWorkbookFromGist, Workbook } from '../Workbook'; +import { loadWorkbookFromString, loadWorkbookFromWorkbookPackage, loadWorkbookFromGist, loadWorkbookFromUrl, Workbook } from '../Workbook'; export interface WorkbookShellContext { session: WorkbookSession rendererRegistry: ResultRendererRegistry } +export interface WorkbookShellProps { + contentString?: string + contentUrl?: string +} + interface WorkbookShellState { isPackageDialogHidden: boolean } -export class WorkbookShell extends React.Component { +export class WorkbookShell extends React.Component { private shellContext: WorkbookShellContext private commandBar: WorkbookCommandBar | null = null private workbookEditor: WorkbookEditor | null = null @@ -53,6 +58,7 @@ export class WorkbookShell extends React.Component { this.saveWorkbook = this.saveWorkbook.bind(this) this.dumpDraftState = this.dumpDraftState.bind(this) this.triggerGistPicker = this.triggerGistPicker.bind(this) + this.loadInitialContent = this.loadInitialContent.bind(this) this.shellContext = { session: new WorkbookSession, @@ -67,13 +73,32 @@ export class WorkbookShell extends React.Component { private onSessionEvent(session: WorkbookSession, sessionEvent: SessionEvent) { if (sessionEvent.kind === SessionEventKind.Ready) { this.workspaceAvailable = true - if (this.workbookEditor) - this.workbookEditor.setUpInitialState() + if (this.workbookEditor && !this.props.contentString && !this.props.contentUrl) { + this.workbookEditor.setUpInitialState() + } + // Fire and forget content load + this.loadInitialContent(); } else { this.workspaceAvailable = false } } + async loadInitialContent() { + if (this.props.contentUrl && this.workbookEditor) { + if (this.props.contentUrl.startsWith('/api/gist')) + this.workbook = await loadWorkbookFromGist(this.shellContext.session, this.props.contentUrl) + else + this.workbook = await loadWorkbookFromUrl(this.shellContext.session, this.props.contentUrl) + + await this.workbookEditor.loadNewContent(this.workbook.markdownContent) + this.restoreNuGetPackages() + } else if (this.props.contentString && this.workbookEditor) { + this.workbook = await loadWorkbookFromString (this.shellContext.session, "Aco", this.props.contentString) + await this.workbookEditor.loadNewContent(this.workbook.markdownContent) + this.restoreNuGetPackages() + } + } + async componentDidMount() { this.shellContext.session.sessionEvent.addListener(this.onSessionEvent) diff --git a/Clients/Xamarin.Interactive.Client.Web/ClientApp/routes.tsx b/Clients/Xamarin.Interactive.Client.Web/ClientApp/routes.tsx index 285bf9292..8f49d5a37 100644 --- a/Clients/Xamarin.Interactive.Client.Web/ClientApp/routes.tsx +++ b/Clients/Xamarin.Interactive.Client.Web/ClientApp/routes.tsx @@ -1,10 +1,104 @@ import * as React from 'react'; -import { Route } from 'react-router-dom'; -import { Layout } from './components/Layout'; +import { Route, Link, RouteComponentProps, RouteProps, match } from 'react-router-dom'; +import { Layout, LayoutProps } from './components/Layout'; +import { ImageFit } from 'office-ui-fabric-react/lib/Image' import { Home } from './components/Home'; import { ComponentPlayground } from './components/ComponentPlayground'; +import { WorkbookShell } from './components/WorkbookShell'; +import { DocumentCard, DocumentCardTitle, DocumentCardLogo, DocumentCardPreview } from 'office-ui-fabric-react/lib/DocumentCard'; +import { createLocation } from "history"; + +export interface CardProps extends RouteProps { + cardId: string, +} + +export interface CardCatalog { + cardId: string + title: string + icon?: string + contentString?: string + contentUrl?: string +} + +export class CardItem extends React.Component, {}> { + getProps(cardId: string): any { + var match = Catalog.Cards.find((card) => { + return card.cardId === cardId + }) + + if (match === undefined) + return {} + + return { + contentUrl: match.contentUrl, + contentString: match.contentString + } + } + + public render() { + return + } +} +export class Catalog extends React.Component, { cards: CardCatalog[] }> { + static loaded = false + constructor() { + super(); + this.state = { cards: Catalog.Cards } + } + + public componentDidMount() { + var t = fetch('/api/workbook') + .then(response => response.json()) + .then((newCards) => { + if (newCards) { + Catalog.Cards = newCards.map((card: CardCatalog, key: any) => { + //var old = Catalog.getCatalog(card.cardId) + return { ...card } + }) + } + this.setState ({cards: newCards}) + }) + } + + public static Cards: CardCatalog [] = [] + + public render() { + return
+

Workbooks

+
+
+ {this.state.cards.map((card, key) => { + const location = createLocation(`/live/${card.cardId}`, null, undefined, this.props.history.location) + const url = this.props.history.createHref (location) + const previewProps = { + previewImages: [ + { + width: 200, + height: 200, + previewImageSrc: card.icon || 'Icon.png', + imageFit: ImageFit.contain + } + ] + }; + + return ( +
+ { this.props.history.push(url)}}> + + + +
+ ) + })} +
+
+

Select a workbook to begin

} /> +
+ } +} export const routes = - + + -; \ No newline at end of file + \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/Controllers/WookbookController.cs b/Clients/Xamarin.Interactive.Client.Web/Controllers/WookbookController.cs new file mode 100644 index 000000000..73a3f1b2a --- /dev/null +++ b/Clients/Xamarin.Interactive.Client.Web/Controllers/WookbookController.cs @@ -0,0 +1,115 @@ +// +// Author: +// Larry Ewing +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.IO; +using System.Collections.Generic; +using System; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Hosting; + +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Configuration; + +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; + +using Xamarin.Interactive.Core; +using Xamarin.Interactive.Workbook.Models; + +namespace Xamarin.Interactive.Client.Web.Controllers +{ + public class Card { + public string CardId { get; set; } + public string Icon { get; set; } + public string ContentUrl { get; set; } + public string ContentString { get; set; } + public string Guid { get; set; } + public string Title { get; set; } + public string Name { get; set; } + //public ImmutableArray PlatformTargets { get; set; } + //public ImmutableArray Packages { get; set; } + } + + [Route("/api/workbook")] + public sealed class WorkbookController : Controller + { + readonly IConfiguration configuration; + readonly IHostingEnvironment hosting; + + public WorkbookController (IConfiguration configuration, IHostingEnvironment hosting) + { + this.configuration = configuration; + this.hosting = hosting; + } + + public IActionResult Index() + { + var workbooks = configuration.GetSection("workbooks"); + var workbookDir = workbooks.GetValue ("path"); + + if (String.IsNullOrEmpty (workbookDir)) + return new JsonResult(new { }); + + var fileProvider = hosting.WebRootFileProvider; + var path = fileProvider.GetFileInfo (workbookDir); + var dir = fileProvider.GetDirectoryContents (workbookDir); + + var cards = new Dictionary(); + foreach (var book in workbooks.GetSection ("books").GetChildren ()) { + var card = new Card + { + CardId = book.GetValue(nameof(Card.CardId)), + ContentUrl = book.GetValue(nameof(Card.ContentUrl)), + ContentString = book.GetValue(nameof(Card.ContentString)), + Title = book.GetValue (nameof (Card.Title)), + Name = book.GetValue (nameof (Card.Name)), + Icon = book.GetValue (nameof (Card.Icon)) + }; + if (!String.IsNullOrEmpty (card.Name)) + cards[card.Name] = card; + + card.CardId = card.CardId ?? card.Name.Replace(".workbook", ""); + } + + var contents = dir.Where (book => !book.IsDirectory && book.PhysicalPath.EndsWith (".workbook")).Select(book => + { + var reader = new StreamReader(book.CreateReadStream ()); + var document = new WorkbookDocument(); + document.Read(reader); + + var manifest = new WorkbookDocumentManifest(); + manifest.Read(document); + + return new Card + { + CardId = book.Name.Replace(".workbook", ""), + ContentUrl = book.PhysicalPath.Replace (hosting.WebRootPath, ""), + Name = book.Name, + Title = manifest.Title, + Guid = manifest.Guid.ToString (), + }; + }); + + foreach (var card in contents) { + if (cards.TryGetValue (card.Name, out var configCard)) { + configCard.Guid = card.Guid; + configCard.Title = card.Title ?? configCard.Title; + configCard.ContentUrl = card.ContentUrl; + configCard.CardId = configCard.CardId ?? configCard.Name.Replace(".workbook", ""); + } else { + cards[card.Name] = card; + } + } + + return new JsonResult(cards.Values); + } + } +} \ No newline at end of file diff --git a/Clients/Xamarin.Interactive.Client.Web/Startup.cs b/Clients/Xamarin.Interactive.Client.Web/Startup.cs index 7ab16de91..e7308c6cc 100644 --- a/Clients/Xamarin.Interactive.Client.Web/Startup.cs +++ b/Clients/Xamarin.Interactive.Client.Web/Startup.cs @@ -14,8 +14,10 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SpaServices.Webpack; +using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.DependencyInjection; using Xamarin.Interactive.Client.Web.Hosting; @@ -27,8 +29,12 @@ namespace Xamarin.Interactive.Client.Web { public sealed class Startup { - public Startup (IConfiguration configuration) - => Configuration = configuration; + IHostingEnvironment environment; + public Startup(IHostingEnvironment env) + { + environment = env; + + } public IConfiguration Configuration { get; } @@ -59,6 +65,8 @@ public void ConfigureServices (IServiceCollection services) .AddJsonProtocol (options => { options.PayloadSerializerSettings = new ExternalInteractiveJsonSerializerSettings (); }); + + services.AddSingleton (environment.ContentRootFileProvider); } public void Configure ( @@ -66,6 +74,8 @@ public void Configure ( IHostingEnvironment env, IServiceProvider serviceProvider) { + environment = env; + if (env.IsDevelopment ()) { app.UseDeveloperExceptionPage (); app.UseWebpackDevMiddleware (new WebpackDevMiddlewareOptions { @@ -84,8 +94,11 @@ public void Configure ( return next (); }); + var provider = new FileExtensionContentTypeProvider(); + provider.Mappings[".workbook"] = "application/workbook"; + app.UseMonacoMuting (); - app.UseStaticFiles (); + app.UseStaticFiles (new StaticFileOptions{ ContentTypeProvider = provider }); app.UseWasmStaticFiles (); app.UseCookiePolicy (); @@ -104,4 +117,4 @@ public void Configure ( }); } } -} \ No newline at end of file +} diff --git a/Clients/Xamarin.Interactive.Client.Web/Views/Shared/_Layout.cshtml b/Clients/Xamarin.Interactive.Client.Web/Views/Shared/_Layout.cshtml index b5b01aeaa..e8a2e06b1 100644 --- a/Clients/Xamarin.Interactive.Client.Web/Views/Shared/_Layout.cshtml +++ b/Clients/Xamarin.Interactive.Client.Web/Views/Shared/_Layout.cshtml @@ -6,7 +6,6 @@ @ViewData["Title"] - diff --git a/Clients/Xamarin.Interactive.Client.Web/appsettings.json b/Clients/Xamarin.Interactive.Client.Web/appsettings.json index 0804371b7..4958947f3 100644 --- a/Clients/Xamarin.Interactive.Client.Web/appsettings.json +++ b/Clients/Xamarin.Interactive.Client.Web/appsettings.json @@ -3,5 +3,36 @@ "LogLevel": { "Default": "Warning" } + }, + "Workbooks": { + "Path": "/books", + "Books": [ + { + "Name": "bean.workbook", + "Icon": "test", + "title": "bean", + "cardId": "dweewe" + }, + { + "name": "outsider", + "icon": "xamarin-inspector-256.png", + "cardId": "been", + "title": "local file url in wwwroot", + "contentUrl": "/books/index.workbook" + }, + { + "name": "gistor", + "cardId": "gist", + "title": "gist card of 89166aa34d93bcf4c5fc4b0d6dd9f02c", + "contentUrl": "/api/gist/lewing/89166aa34d93bcf4c5fc4b0d6dd9f02c" + }, + { + "name": "Taco Tuesday", + "cardId": "taco", + "title": "taco title", + "icon": "books/image.png", + "contentString": "---\nuti: com.xamarin.workbook\nid: e09ea3f1-531e-4b12-87ea-2cebe98cf5aa\ntitle: Wello\nplatform: webassembly-monowebassembly\nplatforms:\n- webassembly-monowebassembly\n---\n\n# Hello Workbooks!\n\n```csharp\n1+1\n```" + } + ] } } diff --git a/Clients/Xamarin.Interactive.Client.Web/wwwroot/Icon.png b/Clients/Xamarin.Interactive.Client.Web/wwwroot/Icon.png new file mode 100644 index 000000000..d4a6b648e Binary files /dev/null and b/Clients/Xamarin.Interactive.Client.Web/wwwroot/Icon.png differ diff --git a/Clients/Xamarin.Interactive.Client.Web/wwwroot/books/bean.workbook b/Clients/Xamarin.Interactive.Client.Web/wwwroot/books/bean.workbook new file mode 100644 index 000000000..d811b0c6d --- /dev/null +++ b/Clients/Xamarin.Interactive.Client.Web/wwwroot/books/bean.workbook @@ -0,0 +1,121 @@ +--- +uti: com.xamarin.workbook +id: 99aae9a9-e0b2-43a8-ba39-7b2f4c5e3a6f +title: CogTweetOcar2 +platforms: +- DotNetCore +packages: +- id: Newtonsoft.Json + version: 10.0.3 +--- + +```csharp +#r "Newtonsoft.Json" +#load "AuthKeys.csx" +``` + +```csharp +using System.Net.Http; +using System.Net.Http.Headers; +using System.IO; +using System.Text; +using System.Linq; +using System.Threading.Tasks; +using System.Text.RegularExpressions; + +HttpClient client = new HttpClient(); + +var tweet = "https://twitter.com/zeynep/status/909421359117324288"; + +// Parse the html with a regex. Don't ever do this 😉 +var sources = Regex.Matches (await client.GetStringAsync (tweet), " m.Groups[1].Value).Distinct (); + +var items = await Task.WhenAll (sources.Select (async (source) => new { + source, + imageData = await client.GetByteArrayAsync (source) +})); +``` + +```csharp +// Request parameters. A third optional parameter is "details" +const string uriBase = "https://westcentralus.api.cognitive.microsoft.com/vision/v1.0/ocr"; +string requestParameters = "language=unk&detectOrientation=true"; + +// Assemble the URI for the REST API Call. +string uri = $"{uriBase}?{requestParameters}"; + +async Task OCR (byte [] imageBytes) +{ + using (ByteArrayContent content = new ByteArrayContent(imageBytes)) + { + content.Headers.Add ("Ocp-Apim-Subscription-Key", PersonalSubscriptions.CognitiveImageService); + content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + var response = await client.PostAsync(uri, content); + return await response.Content.ReadAsStringAsync(); + } +} + +var analysis = await Task.WhenAll (items.Select (async (source) => new { + source, + analysis = await OCR (source.imageData) +})); +``` + +```csharp +using Newtonsoft.Json; + +string ExtractText (string response) { + var json = JsonConvert.DeserializeAnonymousType(response, + new { + regions = new [] { new { + lines = new [] { new { + words = new [] { new { + text = String.Empty + }} + }} + }} + }); + return json.regions.SelectMany (r => r.lines.Select (l => l.words.Select (w => w.text).Aggregate ((a, b) => a + " " + b))).Aggregate ((a, b) => a + "\n" + b); +} +``` + +```csharp +class Result { + public Xamarin.Interactive.Representations.Image image { get; set; } + public Xamarin.Interactive.Representations.VerbatimHtml text { get; set; } + public double sentiment { get; set; } +} + +var conversions = analysis.Select (a => new Result { + image = Xamarin.Interactive.Representations.Image.FromData (a.source.imageData), + text = new Xamarin.Interactive.Representations.VerbatimHtml ($"
{ExtractText (a.analysis)}
") +}).ToArray (); +``` + +```csharp +async Task Sentiment (string [] texts) +{ + string uri = "https://westus.api.cognitive.microsoft.com/text/analytics/v2.0/sentiment"; + var documents = new { + documents = Enumerable.Range (0, texts.Count()).Select (i => new { + language = "en", + id = i.ToString (), + text = texts[i] + }).ToArray () + }; + + using (var content = new StringContent (JsonConvert.SerializeObject (documents), System.Text.Encoding.Default, "text/json")) { + content.Headers.Add ("Ocp-Apim-Subscription-Key", PersonalSubscriptions.CognitiveTextService); + var response = await client.PostAsync (uri, content); + var contentText = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeAnonymousType (contentText, new { documents = new [] { new { score = 0.0, id = "" }}}).documents.Select (d => d.score).ToArray(); + } +} + +var scores = await Sentiment (analysis.Select (a => ExtractText (a.analysis)).ToArray()); +for (var i = 0; i < conversions.Length; i++) { + conversions[i].sentiment = scores[i]; +} +conversions +``` diff --git a/Clients/Xamarin.Interactive.Client.Web/wwwroot/books/image.png b/Clients/Xamarin.Interactive.Client.Web/wwwroot/books/image.png new file mode 100644 index 000000000..5366fb91e Binary files /dev/null and b/Clients/Xamarin.Interactive.Client.Web/wwwroot/books/image.png differ diff --git a/Clients/Xamarin.Interactive.Client.Web/wwwroot/books/index.workbook b/Clients/Xamarin.Interactive.Client.Web/wwwroot/books/index.workbook new file mode 100644 index 000000000..299eceaee --- /dev/null +++ b/Clients/Xamarin.Interactive.Client.Web/wwwroot/books/index.workbook @@ -0,0 +1,121 @@ +--- +uti: com.xamarin.workbook +id: 995fe9a9-e0b2-43a8-ba39-7b2f4c5e3a6f +title: CogTweetOcr +platforms: +- DotNetCore +packages: +- id: Newtonsoft.Json + version: 10.0.3 +--- + +```csharp +#r "Newtonsoft.Json" +#load "AuthKeys.csx" +``` + +```csharp +using System.Net.Http; +using System.Net.Http.Headers; +using System.IO; +using System.Text; +using System.Linq; +using System.Threading.Tasks; +using System.Text.RegularExpressions; + +HttpClient client = new HttpClient(); + +var tweet = "https://twitter.com/zeynep/status/909421359117324288"; + +// Parse the html with a regex. Don't ever do this 😉 +var sources = Regex.Matches (await client.GetStringAsync (tweet), " m.Groups[1].Value).Distinct (); + +var items = await Task.WhenAll (sources.Select (async (source) => new { + source, + imageData = await client.GetByteArrayAsync (source) +})); +``` + +```csharp +// Request parameters. A third optional parameter is "details" +const string uriBase = "https://westcentralus.api.cognitive.microsoft.com/vision/v1.0/ocr"; +string requestParameters = "language=unk&detectOrientation=true"; + +// Assemble the URI for the REST API Call. +string uri = $"{uriBase}?{requestParameters}"; + +async Task OCR (byte [] imageBytes) +{ + using (ByteArrayContent content = new ByteArrayContent(imageBytes)) + { + content.Headers.Add ("Ocp-Apim-Subscription-Key", PersonalSubscriptions.CognitiveImageService); + content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + var response = await client.PostAsync(uri, content); + return await response.Content.ReadAsStringAsync(); + } +} + +var analysis = await Task.WhenAll (items.Select (async (source) => new { + source, + analysis = await OCR (source.imageData) +})); +``` + +```csharp +using Newtonsoft.Json; + +string ExtractText (string response) { + var json = JsonConvert.DeserializeAnonymousType(response, + new { + regions = new [] { new { + lines = new [] { new { + words = new [] { new { + text = String.Empty + }} + }} + }} + }); + return json.regions.SelectMany (r => r.lines.Select (l => l.words.Select (w => w.text).Aggregate ((a, b) => a + " " + b))).Aggregate ((a, b) => a + "\n" + b); +} +``` + +```csharp +class Result { + public Xamarin.Interactive.Representations.Image image { get; set; } + public Xamarin.Interactive.Representations.VerbatimHtml text { get; set; } + public double sentiment { get; set; } +} + +var conversions = analysis.Select (a => new Result { + image = Xamarin.Interactive.Representations.Image.FromData (a.source.imageData), + text = new Xamarin.Interactive.Representations.VerbatimHtml ($"
{ExtractText (a.analysis)}
") +}).ToArray (); +``` + +```csharp +async Task Sentiment (string [] texts) +{ + string uri = "https://westus.api.cognitive.microsoft.com/text/analytics/v2.0/sentiment"; + var documents = new { + documents = Enumerable.Range (0, texts.Count()).Select (i => new { + language = "en", + id = i.ToString (), + text = texts[i] + }).ToArray () + }; + + using (var content = new StringContent (JsonConvert.SerializeObject (documents), System.Text.Encoding.Default, "text/json")) { + content.Headers.Add ("Ocp-Apim-Subscription-Key", PersonalSubscriptions.CognitiveTextService); + var response = await client.PostAsync (uri, content); + var contentText = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeAnonymousType (contentText, new { documents = new [] { new { score = 0.0, id = "" }}}).documents.Select (d => d.score).ToArray(); + } +} + +var scores = await Sentiment (analysis.Select (a => ExtractText (a.analysis)).ToArray()); +for (var i = 0; i < conversions.Length; i++) { + conversions[i].sentiment = scores[i]; +} +conversions +``` \ No newline at end of file